chip via ; {var} are plain interpolations.
/**
@@ -6,6 +7,7 @@
* `{var}` are plain interpolations.
*/
export const en = {
+ ...desktopCompatibilityCopy("en"),
"nav.claude": "Claude",
"claude.tabAccount": "Account",
"claude.tabSettings": "Settings",
diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts
index eb629193027..249b0e63ee1 100644
--- a/gui/src/i18n/fr.ts
+++ b/gui/src/i18n/fr.ts
@@ -1,9 +1,11 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/**
* French i18n catalog. Must match the `TKey` set.
*/
export const fr: Record = {
+ ...desktopCompatibilityCopy("fr"),
"nav.claude": "Claude",
"claude.tabAccount": "Compte",
"claude.tabSettings": "Paramètres",
diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts
index a298a02b15e..9de14d34f66 100644
--- a/gui/src/i18n/ja.ts
+++ b/gui/src/i18n/ja.ts
@@ -1,9 +1,11 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/**
* Japanese i18n catalog; must match the `TKey` set (compile-checked).
*/
export const ja: Record = {
+ ...desktopCompatibilityCopy("ja"),
"nav.claude": "Claude",
"claude.tabAccount": "アカウント",
"claude.tabSettings": "設定",
diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts
index d32503633ed..679b504bf22 100644
--- a/gui/src/i18n/ko.ts
+++ b/gui/src/i18n/ko.ts
@@ -1,9 +1,11 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/**
* Korean i18n catalog; must match the `TKey` set (compile-checked).
*/
export const ko: Record = {
+ ...desktopCompatibilityCopy("ko"),
"nav.claude": "Claude",
"claude.tabAccount": "계정",
"claude.tabSettings": "기타 설정",
diff --git a/gui/src/i18n/pt.ts b/gui/src/i18n/pt.ts
index 8916a78ba48..b5a67f0aa03 100644
--- a/gui/src/i18n/pt.ts
+++ b/gui/src/i18n/pt.ts
@@ -1,3 +1,4 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
// Brazilian Portuguese — generated from en.ts. Must match TKey set (compile-checked).
import type { TKey } from "./en";
@@ -6,6 +7,7 @@ import type { TKey } from "./en";
* Technical terms and model identifiers intentionally remain English.
*/
export const pt: Record = {
+ ...desktopCompatibilityCopy("pt"),
"compactionRouting.sources": "Origens",
"compactionRouting.sourcesAll": "Todos os modelos de conversa",
"compactionRouting.sourcesSelected": "Somente origens selecionadas",
diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts
index 2dd8ef7f73f..0eddc3bcebc 100644
--- a/gui/src/i18n/ru.ts
+++ b/gui/src/i18n/ru.ts
@@ -1,9 +1,11 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/**
* Russian i18n catalog; must match the `TKey` set (compile-checked).
*/
export const ru: Record = {
+ ...desktopCompatibilityCopy("ru"),
"nav.claude": "Claude",
"claude.tabAccount": "Аккаунт",
"claude.tabSettings": "Настройки",
diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts
index 050daecfe11..ca537fa8b5c 100644
--- a/gui/src/i18n/tr.ts
+++ b/gui/src/i18n/tr.ts
@@ -1,3 +1,4 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
// Turkish — generated from en.ts. Must match TKey set (compile-checked).
import type { TKey } from "./en";
@@ -5,6 +6,7 @@ import type { TKey } from "./en";
* Turkish i18n catalog. Must match the `TKey` set (compile-checked).
*/
export const tr: Record = {
+ ...desktopCompatibilityCopy("tr"),
"nav.claude": "Claude",
"claude.tabAccount": "Hesap",
"claude.tabSettings": "Ayarlar",
diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts
index 3074945f070..658b3218599 100644
--- a/gui/src/i18n/vi.ts
+++ b/gui/src/i18n/vi.ts
@@ -1,3 +1,4 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
// Vietnamese — generated from en.ts. Must match TKey set (compile-checked).
import type { TKey } from "./en";
@@ -6,6 +7,7 @@ import type { TKey } from "./en";
* Technical terms and model identifiers intentionally remain English.
*/
export const vi: Record = {
+ ...desktopCompatibilityCopy("vi"),
"nav.claude": "Claude",
"claude.tabAccount": "Tài khoản",
"claude.tabSettings": "Cài đặt",
diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts
index 8159f43a3bb..8a378ab75be 100644
--- a/gui/src/i18n/zh-TW.ts
+++ b/gui/src/i18n/zh-TW.ts
@@ -1,7 +1,9 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */
export const zhTW: Record = {
+ ...desktopCompatibilityCopy("zh-TW"),
"nav.claude": "Claude",
"claude.tabAccount": "帳戶",
"claude.tabSettings": "設定",
diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts
index 31764564d3c..46bef5b32b0 100644
--- a/gui/src/i18n/zh.ts
+++ b/gui/src/i18n/zh.ts
@@ -1,9 +1,11 @@
+import { desktopCompatibilityCopy } from "./desktop-compatibility-copy";
import type { TKey } from "./en";
/**
* Chinese i18n catalog; must match the `TKey` set (compile-checked).
*/
export const zh: Record = {
+ ...desktopCompatibilityCopy("zh"),
"nav.claude": "Claude",
"claude.tabAccount": "账户",
"claude.tabSettings": "设置",
diff --git a/gui/src/pages/CodexSet.tsx b/gui/src/pages/CodexSet.tsx
index 444b6a7ad6e..7fd05fc2f5d 100644
--- a/gui/src/pages/CodexSet.tsx
+++ b/gui/src/pages/CodexSet.tsx
@@ -2,18 +2,19 @@ import { useEffect, useState } from "react";
import { useT } from "../i18n/shared";
import CodexSetMultiauth from "./codex-set-multiauth";
import CodexSetPrompt from "./codex-set-prompt";
+import CodexDesktopCompatibility from "./codex-desktop-compatibility";
import { codexSetTabKeyDown, readCodexSetTabFromHash, selectCodexSetTab } from "./codex-set-tab";
/**
* Codex Set — the page that configures Codex as a whole, not just its accounts.
*
- * Two exclusive tabpanels shaped like Logs/Debug rather than the scrolling
+ * Exclusive tabpanels shaped like Logs/Debug rather than the scrolling
* SectionTabs strip: Multi-auth and Prompt are unrelated surfaces, and Multi-auth
* polls /api/codex-auth/* on a 30s timer that has no business running while the
* user is editing prompts. Prompt lazy-mounts on first visit and stays mounted
* afterwards, so hopping between tabs does not refetch either side.
*/
-export default function CodexSet({ apiBase }: { apiBase: string }) {
+export default function CodexSet({ apiBase, machineApiBase = apiBase, connected = false }: { apiBase: string; machineApiBase?: string; connected?: boolean }) {
const t = useT();
const [tab, setTab] = useState(readCodexSetTabFromHash);
const [promptMounted, setPromptMounted] = useState(() => readCodexSetTabFromHash() === "prompt");
@@ -22,6 +23,7 @@ export default function CodexSet({ apiBase }: { apiBase: string }) {
// account poll behind a hidden panel - exactly the cost this shell was shaped to
// avoid. Both panels now mount on first selection and stay mounted after.
const [multiauthMounted, setMultiauthMounted] = useState(() => readCodexSetTabFromHash() === "multiauth");
+ const [desktopMounted, setDesktopMounted] = useState(() => readCodexSetTabFromHash() === "desktop");
useEffect(() => {
const onHash = () => setTab(readCodexSetTabFromHash());
@@ -37,6 +39,8 @@ export default function CodexSet({ apiBase }: { apiBase: string }) {
const showMultiauth = multiauthMounted || tab === "multiauth";
if (showPrompt !== promptMounted) setPromptMounted(true);
if (showMultiauth !== multiauthMounted) setMultiauthMounted(true);
+ const showDesktop = desktopMounted || tab === "desktop";
+ if (showDesktop !== desktopMounted) setDesktopMounted(true);
return (
<>
@@ -67,6 +71,10 @@ export default function CodexSet({ apiBase }: { apiBase: string }) {
>
{t("codexSet.tab.prompt")}
+
{showPrompt && (
@@ -90,6 +98,9 @@ export default function CodexSet({ apiBase }: { apiBase: string }) {
)}
+ {showDesktop &&
+
+ }
>
);
}
diff --git a/gui/src/pages/codex-desktop-compatibility.tsx b/gui/src/pages/codex-desktop-compatibility.tsx
new file mode 100644
index 00000000000..83df10b9068
--- /dev/null
+++ b/gui/src/pages/codex-desktop-compatibility.tsx
@@ -0,0 +1,126 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useI18n } from "../i18n/shared";
+import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch";
+import { CompatibilityApiError, readCompatibilityCertificate, readCompatibilityRuntime, readCompatibilitySnapshot, runCompatibilityAction,
+ type CompatibilityAction, type CompatibilitySnapshot } from "../desktop-compatibility-api";
+import { setClientResourceData, useClientResource } from "../client-resource";
+import DesktopCompatibilityStartupSetting from "./desktop-compatibility-startup-setting";
+
+const label = { prepare: "desktopCompat.prepare", trust: "desktopCompat.trust", "remove-trust": "desktopCompat.remove", renew: "desktopCompat.renew",
+ start: "desktopCompat.start", stop: "desktopCompat.stop", launch: "desktopCompat.launch", observe: "desktopCompat.observe", apply: "desktopCompat.apply" } as const;
+const errorLabel = { build_unverified: "desktopCompat.blockedByBuild", egress_proxy_invalid: "desktopCompat.blockedByProxy",
+ renewal_required: "desktopCompat.renew",
+ native_routing_unverified: "desktopCompat.routingChanged",
+ connection_unavailable: "desktopCompat.connectionUnavailable", connection_invalid: "desktopCompat.connectionUnavailable", connection_changed: "desktopCompat.connectionUnavailable",
+ app_running: "desktopCompat.closeApp", local_dashboard_confirmation_required: "desktopCompat.localOnly" } as const;
+
+function CompatibilityPanel({ apiBase, active }: { apiBase: string; active: boolean }) {
+ const { t, locale } = useI18n();
+ const [needsRefresh, setNeedsRefresh] = useState(false), [mutating, setMutating] = useState(false);
+ const [actionError, setActionError] = useState(null), [done, setDone] = useState(false);
+ const [choice, setChoice] = useState(null), [acknowledged, setAcknowledged] = useState(false);
+ const lifecycle = useRef({ alive: true, pending: false, generation: 0, operations: new Set() });
+ const certificateKey = `desktop-compatibility-certificate:${apiBase}`, runtimeKey = `desktop-compatibility-runtime:${apiBase}`;
+ const certificate = useClientResource(certificateKey, signal => readCompatibilityCertificate(apiBase, signal), { enabled: active, deadlineMs: 15000, staleAfterMs: 0 });
+ const runtime = useClientResource(runtimeKey, signal => readCompatibilityRuntime(apiBase, signal), { enabled: active, deadlineMs: 10000, staleAfterMs: 0, pollMs: 5000 });
+ const snapshot: CompatibilitySnapshot | null = certificate.data && runtime.data ? { certificate: certificate.data, runtime: runtime.data } : null;
+ const fresh = !needsRefresh && certificate.lastAttemptOk && runtime.lastAttemptOk && !certificate.refreshing && !runtime.refreshing;
+ const busy = mutating || certificate.loading || runtime.loading;
+ const readError = certificate.error ?? runtime.error;
+ const error = actionError ?? (readError ? readError instanceof CompatibilityApiError ? readError.code : "connection_unconfirmed" : null);
+ useEffect(() => {
+ const current = lifecycle.current; current.alive = true;
+ return () => { current.alive = false; current.generation++; current.pending = false;
+ for (const op of current.operations) { op.controller.abort(); op.clear(); } current.operations.clear(); };
+ }, []);
+ const execute = useCallback(async (action?: CompatibilityAction) => {
+ const current = lifecycle.current;
+ if (current.pending) return;
+ current.pending = true; const generation = ++current.generation;
+ setMutating(true); setActionError(null); setDone(false); setNeedsRefresh(true);
+ const op = createBoundedFetch(action ? 140000 : 15000); current.operations.add(op);
+ try {
+ if (action) await runCompatibilityAction(apiBase, action, op.signal);
+ const value = await readCompatibilitySnapshot(apiBase, op.signal);
+ if (current.alive && current.generation === generation) {
+ setClientResourceData(certificateKey, value.certificate); setClientResourceData(runtimeKey, value.runtime);
+ setNeedsRefresh(false); setDone(!!action); setChoice(null); setAcknowledged(false);
+ }
+ } catch (cause) {
+ if (current.alive && current.generation === generation) { setActionError(cause instanceof CompatibilityApiError ? cause.code : "connection_unconfirmed"); setChoice(null); setAcknowledged(false); }
+ } finally {
+ current.operations.delete(op); op.clear();
+ if (current.generation === generation) { current.pending = false; if (current.alive) setMutating(false); }
+ }
+ }, [apiBase, certificateKey, runtimeKey]);
+
+ const available = !!snapshot?.certificate.supported && !!snapshot.runtime.supported;
+ const idle = snapshot?.runtime.phase === "off" && !snapshot.certificate.busy;
+ const running = snapshot?.runtime.phase === "running";
+ const actions: CompatibilityAction[] = [];
+ if (snapshot && available) {
+ const cert = snapshot.certificate;
+ if (idle && cert.state === "missing") actions.push({ target: "certificate", action: "prepare" });
+ if (idle && cert.fingerprint) {
+ if (cert.state === "prepared") actions.push({ target: "certificate", action: "trust", fingerprint: cert.fingerprint });
+ // Unknown trust may still need fingerprint-verified cleanup; invalid keys cannot be acted on.
+ if (["trusted", "expired", "renewal-required", "unknown"].includes(cert.state)) actions.push({ target: "certificate", action: "remove-trust", fingerprint: cert.fingerprint });
+ if (["prepared", "trusted", "expired", "renewal-required"].includes(cert.state)) actions.push({ target: "certificate", action: "renew", fingerprint: cert.fingerprint });
+ }
+ if (idle && cert.state === "trusted") actions.push({ target: "runtime", action: "start" });
+ if (running) actions.push({ target: "runtime", action: "launch" }, { target: "runtime", action: "observe" }, { target: "runtime", action: "apply" });
+ if (running || snapshot.runtime.phase === "cleanup-required") actions.push({ target: "runtime", action: "stop" });
+ }
+ return
+ {t("desktopCompat.title")}
+ {t("desktopCompat.description")}
+ {t("desktopCompat.localOnly")}
+
+
+ {error && {t(error in errorLabel ? errorLabel[error as keyof typeof errorLabel] : "desktopCompat.error")}
{error}}
+ {done && {t("desktopCompat.done")}
}
+ {snapshot && <>
+ {!available && {t("desktopCompat.unsupported")}
}
+
+ - {t("desktopCompat.certificate")}
{snapshot.certificate.state}
+ {snapshot.certificate.fingerprint && - {snapshot.certificate.fingerprint}
}
+ {snapshot.certificate.expiresAt !== undefined && <>- {t("desktopCompat.expiry")}
- {new Date(snapshot.certificate.expiresAt).toLocaleString(locale)}
>}
+ - {t("desktopCompat.runtime")}
{snapshot.runtime.phase}{snapshot.runtime.usage && <> · {snapshot.runtime.usage.mode}>}
+
+ {snapshot.certificate.renewalDue && {t("desktopCompat.renewalDue")}
}
+ {snapshot.runtime.usage?.mode === "apply" && {t("desktopCompat.trialRisk")}
}
+ {snapshot.runtime.usage?.mode === "observe" && snapshot.runtime.usage.phase.endsWith("awaiting-original-response") && {t("desktopCompat.cacheRefreshHint")}
}
+ {snapshot.runtime.contextFailure && {t(errorLabel[snapshot.runtime.contextFailure])}
}
+ {running && {t("desktopCompat.reconnectHint")}
}
+ {snapshot.runtime.usage?.observation && {t("desktopCompat.observation", {
+ json: snapshot.runtime.usage.observation.jsonSnapshots, sse: snapshot.runtime.usage.observation.streamSnapshots,
+ streams: snapshot.runtime.usage.observation.validatedActiveStreams,
+ })}
}
+ >}
+
+ {actions.map(action => )}
+
+ {choice && }
+ ;
+}
+
+export default function CodexDesktopCompatibility(props: { apiBase: string; active: boolean; connected?: boolean }) {
+ const { t } = useI18n();
+ if (props.connected) return {t("desktopCompat.title")}
{t("desktopCompat.connectedUnavailable")}
;
+ // Never carry a certificate fingerprint or outstanding confirmation to another host.
+ return ;
+}
diff --git a/gui/src/pages/codex-set-tab.ts b/gui/src/pages/codex-set-tab.ts
index 06efaa893ab..193adfb6a66 100644
--- a/gui/src/pages/codex-set-tab.ts
+++ b/gui/src/pages/codex-set-tab.ts
@@ -1,30 +1,28 @@
import type { KeyboardEvent } from "react";
/**
- * Tab state for the Codex Set page, shaped exactly like Logs/Debug: two exclusive
+ * Tab state for the Codex Set page, shaped exactly like Logs/Debug: exclusive
* tabpanels whose choice lives in the hash, not in component state alone. That is
* what makes the tab survive a refresh, a bookmark, and back/forward — and it is
* the pattern devlog 004 §A3 identifies as the one the ask actually names.
*/
-export type CodexSetTab = "multiauth" | "prompt";
+export type CodexSetTab = "multiauth" | "prompt" | "desktop";
+const tabs: readonly CodexSetTab[] = ["multiauth", "prompt", "desktop"];
export function readCodexSetTabFromHash(): CodexSetTab {
- return window.location.hash.replace(/^#\/?/, "") === "codex-set/prompt" ? "prompt" : "multiauth";
+ const hash = window.location.hash.replace(/^#\/?/, "");
+ return hash === "codex-set/desktop" ? "desktop" : hash === "codex-set/prompt" ? "prompt" : "multiauth";
}
export function selectCodexSetTab(next: CodexSetTab): void {
- window.location.hash = next === "prompt" ? "codex-set/prompt" : "codex-set";
+ window.location.hash = next === "multiauth" ? "codex-set" : `codex-set/${next}`;
}
export function codexSetTabKeyDown(e: KeyboardEvent): void {
- if (e.key === "ArrowLeft" || e.key === "Home") {
- e.preventDefault();
- selectCodexSetTab("multiauth");
- document.getElementById("codex-set-tab-multiauth")?.focus();
- } else if (e.key === "ArrowRight" || e.key === "End") {
- e.preventDefault();
- selectCodexSetTab("prompt");
- document.getElementById("codex-set-tab-prompt")?.focus();
- }
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
+ e.preventDefault();
+ const current = tabs.indexOf(readCodexSetTabFromHash());
+ const index = e.key === "Home" ? 0 : e.key === "End" ? tabs.length - 1 : (current + (e.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length;
+ const next = tabs[index]!; selectCodexSetTab(next);
+ document.getElementById(`codex-set-tab-${next}`)?.focus();
}
-
diff --git a/gui/src/pages/desktop-compatibility-startup-setting.tsx b/gui/src/pages/desktop-compatibility-startup-setting.tsx
new file mode 100644
index 00000000000..82f86ed3502
--- /dev/null
+++ b/gui/src/pages/desktop-compatibility-startup-setting.tsx
@@ -0,0 +1,36 @@
+import { useEffect, useRef, useState } from "react";
+import { useT } from "../i18n/shared";
+import { Switch } from "../ui";
+import { setClientResourceData, useClientResource } from "../client-resource";
+import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch";
+import { CompatibilityApiError, readCompatibilityStartupSettings, saveCompatibilityStartupSettings } from "../desktop-compatibility-api";
+
+export default function DesktopCompatibilityStartupSetting({ apiBase, active }: { apiBase: string; active: boolean }) {
+ const t = useT(), key = `desktop-compatibility-startup:${apiBase}`;
+ const resource = useClientResource(key, signal => readCompatibilityStartupSettings(apiBase, signal), { enabled: active, deadlineMs: 15000, staleAfterMs: 0 });
+ const [busy, setBusy] = useState(false), [uncertain, setUncertain] = useState(false), [error, setError] = useState(null);
+ const ownership = useRef({ alive: true, pending: false, operation: null as BoundedFetch | null });
+ useEffect(() => { const owned = ownership.current; owned.alive = true;
+ return () => { owned.alive = false; owned.operation?.controller.abort(); owned.operation?.clear(); }; }, []);
+ async function run(toggle: boolean) {
+ const owner = ownership.current;
+ if (owner.pending || (toggle && !resource.data)) return;
+ owner.pending = true; setBusy(true); setError(null);
+ const op = createBoundedFetch(15000); owner.operation = op;
+ try {
+ if (toggle) await saveCompatibilityStartupSettings(apiBase, resource.data!, !resource.data!.startOnProxyStart, op.signal);
+ const actual = await readCompatibilityStartupSettings(apiBase, op.signal);
+ if (owner.alive) { setClientResourceData(key, actual); setUncertain(false); }
+ } catch (cause) {
+ if (owner.alive) { setUncertain(true); setError(cause instanceof CompatibilityApiError ? cause.code : "connection_unconfirmed"); }
+ } finally { op.clear(); owner.operation = null; owner.pending = false; if (owner.alive) setBusy(false); }
+ }
+ const readError = resource.error instanceof CompatibilityApiError ? resource.error.code : resource.error ? "connection_unconfirmed" : null;
+ return
+ void run(true)} />
+ {t("desktopCompat.autoStartHint")}
+ {(error || readError) && {t("desktopCompat.error")} {error ?? readError}
}
+ {(uncertain || readError) && }
+ ;
+}
diff --git a/gui/tests/codex-set-shell.test.tsx b/gui/tests/codex-set-shell.test.tsx
index b105b97640a..f41d61be8e7 100644
--- a/gui/tests/codex-set-shell.test.tsx
+++ b/gui/tests/codex-set-shell.test.tsx
@@ -99,14 +99,14 @@ function json(value: unknown, status = 200): Response {
return new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
}
-async function mountShell(): Promise<{ root: Root; container: HTMLElement }> {
+async function mountShell(apiBase = "", machineApiBase = apiBase): Promise<{ root: Root; container: HTMLElement }> {
const { createRoot } = await import("react-dom/client");
const container = document.createElement("div");
document.body.append(container);
let root!: Root;
await act(async () => {
root = createRoot(container);
- root.render( );
+ root.render( );
});
return { root, container };
}
@@ -123,7 +123,7 @@ async function mountPrompt(): Promise<{ root: Root; container: HTMLElement }> {
return { root, container };
}
-function panel(container: HTMLElement, name: "multiauth" | "prompt"): HTMLElement | null {
+function panel(container: HTMLElement, name: "multiauth" | "prompt" | "desktop"): HTMLElement | null {
return container.querySelector("#codex-set-panel-" + name);
}
@@ -135,6 +135,25 @@ test("1. #codex-set renders Multi-auth, and Prompt is not mounted", async () =>
expect(multi!.hasAttribute("hidden")).toBe(false);
// Case 4: Prompt does not mount until first visited.
expect(panel(container, "prompt")).toBeNull();
+ expect(panel(container, "desktop")).toBeNull();
+ await act(async () => { root.unmount(); });
+});
+
+test("desktop compatibility deep link mounts only its read-only status surface", async () => {
+ testWindow.location.hash = "#codex-set/desktop";
+ const calls = stubRoutes(call => json(call.url.endsWith("/settings") ? { ok: true, settings: { startOnProxyStart: false, revision: "a".repeat(64) } } : call.url.endsWith("/certificate")
+ ? { ok: true, certificate: { supported: true, state: "missing", busy: null } }
+ : { ok: true, runtime: { supported: true, phase: "off", running: false } }));
+ const { container, root } = await mountShell("/shared", "/machine");
+ expect(hashBelongsToPage("codex-set/desktop", "codex-set")).toBe(true);
+ expect(resolveAppHashChange("codex-set/desktop").replaceTo).toBeNull();
+ expect(panel(container, "desktop")?.hasAttribute("hidden")).toBe(false);
+ expect(panel(container, "multiauth")).toBeNull(); expect(panel(container, "prompt")).toBeNull();
+ expect(new Set(calls.map(call => call.url))).toEqual(new Set([
+ "/machine/api/codex/desktop-compatibility/settings", "/machine/api/codex/desktop-compatibility/certificate",
+ "/machine/api/codex/desktop-compatibility/runtime",
+ ]));
+ expect(calls.every(call => call.method === "GET" && call.url.startsWith("/machine/api/codex/desktop-compatibility/"))).toBe(true);
await act(async () => { root.unmount(); });
});
diff --git a/gui/tests/desktop-compatibility-api.test.ts b/gui/tests/desktop-compatibility-api.test.ts
new file mode 100644
index 00000000000..e6735a77a2e
--- /dev/null
+++ b/gui/tests/desktop-compatibility-api.test.ts
@@ -0,0 +1,50 @@
+import { afterEach, expect, test } from "bun:test";
+import { parseCompatibilityCertificate, parseCompatibilityRuntime, runCompatibilityAction } from "../src/desktop-compatibility-api";
+import { DICTS } from "../src/i18n/catalogs";
+const originalFetch = globalThis.fetch;
+test("observation counters never upgrade response observations into native recovery proof", () => {
+ const observation = { jsonSnapshots: 1, streamSnapshots: 3, validatedActiveStreams: 1, lastSnapshotAt: 1000,
+ sourceProcessVerified: false, composerRecoveryVerified: false };
+ const status = { supported: true, phase: "running", running: true,
+ usage: { mode: "observe", phase: "observing", outputs: 0, appCacheConfirmed: false, observation } };
+ expect(parseCompatibilityRuntime(status).usage?.observation).toEqual(observation);
+ for (const invalid of [{ ...observation, sourceProcessVerified: true }, { ...observation, composerRecoveryVerified: true },
+ { ...observation, streamSnapshots: -1 }, { ...observation, jsonSnapshots: "1" }, { ...observation, lastSnapshotAt: Infinity }]) {
+ expect(() => parseCompatibilityRuntime({ ...status, usage: { ...status.usage, observation: invalid } })).toThrow("invalid_runtime_status");
+ }
+});
+afterEach(() => { globalThis.fetch = originalFetch; });
+test("status projection excludes private/unknown data and rejects coerced states", () => {
+ const certificate = parseCompatibilityCertificate({ supported: true, state: "trusted", busy: null, fingerprint: "A".repeat(64), privateKey: "fixture-secret" });
+ expect(JSON.stringify(certificate)).not.toContain("fixture-secret");
+ expect(() => parseCompatibilityCertificate({ supported: true, state: ["trusted"], busy: null })).toThrow();
+ expect(() => parseCompatibilityRuntime({ supported: true, phase: "off", running: true })).toThrow();
+ expect(() => parseCompatibilityRuntime({ supported: true, phase: ["running"], running: true })).toThrow();
+ expect(parseCompatibilityRuntime({ supported: true, phase: "running", running: true, contextFailure: "native_routing_unverified" }).contextFailure).toBe("native_routing_unverified");
+ expect(() => parseCompatibilityRuntime({ supported: true, phase: "running", running: true, contextFailure: "untrusted-arbitrary-message" })).toThrow();
+});
+test("certificate actions bind the displayed fingerprint and never retry uncertain writes", async () => {
+ const calls: RequestInit[] = [];
+ globalThis.fetch = (async (_input, init) => { calls.push(init!); throw new TypeError("connection lost"); }) as typeof fetch;
+ await expect(runCompatibilityAction("", { target: "certificate", action: "trust", fingerprint: "A".repeat(64) }, new AbortController().signal)).rejects.toThrow();
+ expect(calls).toHaveLength(1);
+ expect(JSON.parse(String(calls[0]!.body))).toEqual({ action: "trust", confirmed: true, fingerprint: "A".repeat(64) });
+ await expect(runCompatibilityAction("", { target: "certificate", action: "renew" }, new AbortController().signal)).rejects.toThrow("certificate_fingerprint_required");
+ expect(calls).toHaveLength(1);
+});
+test("only the deliberate apply action carries account-wide consent", async () => {
+ const bodies: unknown[] = [];
+ globalThis.fetch = (async (_input, init) => { bodies.push(JSON.parse(String(init!.body))); return Response.json({ ok: true }); }) as typeof fetch;
+ await runCompatibilityAction("", { target: "runtime", action: "start" }, new AbortController().signal);
+ await runCompatibilityAction("", { target: "runtime", action: "apply" }, new AbortController().signal);
+ expect(bodies).toEqual([{ action: "start", confirmed: true }, { action: "apply", confirmed: true, accountWideConsent: true }]);
+});
+test("every supported locale contains the complete compatibility consent namespace", () => {
+ const keys = Object.keys(DICTS.en).filter(key => key.startsWith("desktopCompat."));
+ expect(keys.length).toBeGreaterThan(20);
+ for (const [locale, catalog] of Object.entries(DICTS)) {
+ expect(Object.keys(catalog).filter(key => key.startsWith("desktopCompat."))).toEqual(keys);
+ for (const key of keys) expect((catalog as Record)[key]?.length).toBeGreaterThan(0);
+ if (locale !== "en") expect(catalog["desktopCompat.trialRisk"]).not.toBe(DICTS.en["desktopCompat.trialRisk"]);
+ }
+});
diff --git a/gui/tests/desktop-compatibility-panel.test.tsx b/gui/tests/desktop-compatibility-panel.test.tsx
new file mode 100644
index 00000000000..3f0d3485a4a
--- /dev/null
+++ b/gui/tests/desktop-compatibility-panel.test.tsx
@@ -0,0 +1,147 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { Window } from "happy-dom";
+import { act, StrictMode } from "react";
+import type { Root } from "react-dom/client";
+import { LanguageProvider } from "../src/i18n/provider";
+import CodexDesktopCompatibility from "../src/pages/codex-desktop-compatibility";
+import { clearClientResourceStoresForTests } from "../src/client-resource";
+
+const keys = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const;
+let previous: Record, page: Window, root: Root | undefined;
+const originalFetch = globalThis.fetch;
+beforeEach(() => {
+ clearClientResourceStoresForTests();
+ previous = Object.fromEntries(keys.map(key => [key, Reflect.get(globalThis, key)]));
+ page = new Window({ url: "http://localhost/#codex-set/desktop" });
+ page.localStorage.setItem("ocx-lang", "en");
+ for (const key of keys) Object.defineProperty(globalThis, key, { configurable: true, value: key === "IS_REACT_ACT_ENVIRONMENT" ? true : Reflect.get(page, key) });
+});
+afterEach(async () => {
+ if (root) await act(async () => root!.unmount()); root = undefined;
+ globalThis.fetch = originalFetch; page.close();
+ for (const key of keys) Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] });
+});
+const requests: { url: string; method: string; body?: unknown }[] = [];
+function server(uncertain = false, certificateState?: string, usagePhase?: string, observation?: Record) {
+ requests.length = 0; let trusted = false, startup = false;
+ globalThis.fetch = (async (input, init) => {
+ const url = String(input), method = init?.method ?? "GET";
+ requests.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined });
+ if (url.endsWith("/settings")) {
+ if (method === "POST") { startup = JSON.parse(String(init!.body)).startOnProxyStart; if (uncertain) throw new TypeError("uncertain response"); }
+ return Response.json({ ok: true, settings: { startOnProxyStart: startup, revision: (startup ? "b" : "a").repeat(64) } });
+ }
+ if (method === "POST") { trusted = true; if (uncertain) throw new TypeError("uncertain response"); return Response.json({ ok: true }); }
+ return Response.json(url.endsWith("/certificate") ? { ok: true, certificate: { supported: true, state: trusted ? "trusted" : certificateState ?? "prepared", busy: null,
+ fingerprint: (url.startsWith("/second") ? "B" : "A").repeat(64) } } : { ok: true, runtime: { supported: true, phase: usagePhase ? "running" : "off", running: !!usagePhase,
+ ...(usagePhase ? { usage: { mode: "observe", phase: usagePhase, outputs: 0, appCacheConfirmed: false, ...(observation ? { observation } : {}) } } : {}) } });
+ }) as typeof fetch;
+}
+async function mount(apiBase = "") {
+ const { createRoot } = await import("react-dom/client");
+ const container = document.createElement("div"); document.body.append(container); root = createRoot(container);
+ await render(apiBase); return container;
+}
+async function render(apiBase: string, connected = false) {
+ await act(async () => { root!.render( ); });
+}
+function button(container: HTMLElement, text: string): HTMLButtonElement {
+ const value = [...container.querySelectorAll("button")].find(node => node.textContent === text);
+ if (!value) throw new Error("Missing button: " + text); return value;
+}
+async function chooseAndConfirm(container: HTMLElement) {
+ await act(async () => button(container, "Register trust").click());
+ expect(requests.filter(req => req.method === "POST")).toHaveLength(0);
+ expect(button(container, "Confirm action").disabled).toBe(true);
+ await act(async () => (container.querySelector("input[type=checkbox]") as HTMLInputElement).click());
+ await act(async () => button(container, "Confirm action").click());
+}
+test("StrictMode status reads are inert and a fingerprint-bound action needs explicit consent", async () => {
+ server(); const container = await mount();
+ expect(button(container, "Register trust").disabled).toBe(false);
+ expect(requests.every(req => req.method === "GET")).toBe(true);
+ await chooseAndConfirm(container);
+ const posts = requests.filter(req => req.method === "POST"); expect(posts).toHaveLength(1);
+ expect(posts[0]!.body).toEqual({ action: "trust", confirmed: true, fingerprint: "A".repeat(64) });
+ expect(button(container, "Start observation").disabled).toBe(false);
+ await act(async () => button(container, "Start observation").click());
+ expect(requests.filter(req => req.method === "POST").at(-1)?.body).toEqual({ action: "start", confirmed: true });
+ expect(container.querySelector("fieldset")).toBeNull();
+});
+
+test.each(["expired-awaiting-original-response", "observing-awaiting-original-response"])("%s does not claim the native cache has reverted", async phase => {
+ server(false, "trusted", phase); const container = await mount();
+ expect(container.textContent).toContain("Stopping correction does not immediately reset the Codex display.");
+ expect(container.textContent).toContain("this panel cannot confirm that refresh.");
+ expect(requests.every(req => req.method === "GET")).toBe(true);
+});
+
+test("observed response counts are displayed without claiming a native process or recovered composer", async () => {
+ server(false, "trusted", "observing", { jsonSnapshots: 2, streamSnapshots: 3, validatedActiveStreams: 1, lastSnapshotAt: 1000,
+ sourceProcessVerified: false, composerRecoveryVerified: false });
+ const container = await mount();
+ expect(container.textContent).toContain("JSON 2, SSE 3; active bound streams: 1");
+ expect(container.textContent).toContain("Counts do not identify the sending process or prove composer recovery.");
+ expect(requests.every(req => req.method === "GET")).toBe(true);
+});
+
+test("trial confirmation explains delayed native refresh before any correction request", async () => {
+ server(false, "trusted", "observing"); const container = await mount();
+ expect(container.textContent).not.toContain("Stopping correction does not immediately reset the Codex display.");
+ await act(async () => button(container, "Run 3-minute trial").click());
+ expect(container.querySelector("fieldset")?.textContent).toContain("Codex must receive fresh usage data");
+ expect(button(container, "Confirm action").disabled).toBe(true);
+ expect(requests.every(req => req.method === "GET")).toBe(true);
+});
+
+test.each([
+ ["prepared", false, true], ["trusted", true, true], ["expired", true, true], ["renewal-required", true, true], ["unknown", true, false], ["invalid", false, false],
+] as const)("certificate state %s exposes only supported cleanup and renewal actions", async (state, remove, renew) => {
+ server(false, state); const container = await mount();
+ const labels = [...container.querySelectorAll("button")].map(value => value.textContent);
+ expect(labels.includes("Remove trust")).toBe(remove); expect(labels.includes("Renew certificate")).toBe(renew);
+ if (state === "renewal-required") expect(labels.includes("Start observation")).toBe(false);
+ expect(requests.filter(value => value.method === "POST")).toHaveLength(0);
+});
+test("a lost write response disables replay until a fresh read proves the actual state", async () => {
+ server(true); const container = await mount(); await chooseAndConfirm(container);
+ expect(container.querySelector('[role="alert"]')).not.toBeNull();
+ expect(button(container, "Register trust").disabled).toBe(true);
+ expect(requests.filter(req => req.method === "POST")).toHaveLength(1);
+ await act(async () => button(container, "Refresh status").click());
+ expect(button(container, "Start observation").disabled).toBe(false);
+ expect(requests.filter(req => req.method === "POST")).toHaveLength(1);
+});
+test("changing target drops an outstanding certificate confirmation", async () => {
+ server(); const container = await mount();
+ await act(async () => button(container, "Register trust").click());
+ await act(async () => (container.querySelector("input[type=checkbox]") as HTMLInputElement).click());
+ await render("/second");
+ expect(container.querySelector("fieldset")).toBeNull(); expect(container.textContent).toContain("B".repeat(64));
+ expect(requests.filter(req => req.method === "POST")).toHaveLength(0);
+});
+test("managed client mode never falls back to mutating the shared hub", async () => {
+ server(); const container = await mount(); requests.length = 0;
+ await render("/machine", true);
+ expect(container.textContent).toContain("unavailable in OpenCodex managed client mode");
+ expect(requests).toHaveLength(0); expect(container.querySelector("button")).toBeNull();
+});
+test("auto-start toggle writes once with the displayed revision and rechecks the saved state", async () => {
+ server(); const container = await mount();
+ const toggle = button(container, "Resume observation when OpenCodex starts");
+ expect(toggle.getAttribute("aria-pressed")).toBe("false");
+ await act(async () => toggle.click());
+ expect(toggle.getAttribute("aria-pressed")).toBe("true");
+ const posts = requests.filter(req => req.method === "POST"); expect(posts).toHaveLength(1);
+ expect(posts[0]!.url).toEndWith("/settings"); expect(posts[0]!.body).toEqual({ confirmed: true, startOnProxyStart: true, revision: "a".repeat(64) });
+ expect(requests.some(req => req.method === "POST" && req.url.endsWith("/runtime"))).toBe(false);
+});
+test("an uncertain auto-start write is not replayed and refresh recovers its committed state", async () => {
+ server(true); const container = await mount();
+ const toggle = button(container, "Resume observation when OpenCodex starts");
+ await act(async () => toggle.click()); expect(toggle.disabled).toBe(true);
+ const setting = toggle.closest(".panel")!;
+ await act(async () => button(setting as HTMLElement, "Refresh status").click());
+ expect(toggle.disabled).toBe(false); expect(toggle.getAttribute("aria-pressed")).toBe("true");
+ expect(requests.filter(req => req.method === "POST")).toHaveLength(1);
+});
diff --git a/gui/tests/sidebar-codex-set.test.ts b/gui/tests/sidebar-codex-set.test.ts
index 1d7c8b4217b..ec6f3963ad8 100644
--- a/gui/tests/sidebar-codex-set.test.ts
+++ b/gui/tests/sidebar-codex-set.test.ts
@@ -32,7 +32,7 @@ test("Codex Set is always present in the sidebar, never filtered by view mode",
* written to catch. The entry's identity is its id and its label key.
*/
expect(src).toContain('{ id: "codex-set", tkey: "nav.codexSet", Icon:');
- expect(src).toContain('{page === "codex-set" && }');
+ expect(src).toContain('{page === "codex-set" && }');
});
test("the shipped #codex-auth bookmark still resolves", async () => {
diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json
index 39c908bd9fd..0eaef2c6f9e 100644
--- a/scripts/test-layout/layout.json
+++ b/scripts/test-layout/layout.json
@@ -802,6 +802,10 @@
"desktop-3p.test.ts": "clients",
"desktop-app-restart.test.ts": "clients",
"desktop-cli-contracts.test.ts": "clients",
+ "desktop-compatibility-authority.test.ts": "clients", "desktop-compatibility-certificate-service.test.ts": "clients", "desktop-compatibility-trust.test.ts": "clients", "desktop-compatibility-build-probe.test.ts": "clients", "desktop-compatibility-native-identity.test.ts": "clients",
+ "desktop-compatibility-runtime.test.ts": "clients", "desktop-compatibility-relay.test.ts": "clients", "desktop-compatibility-routing.test.ts": "clients", "desktop-compatibility-connection-store.test.ts": "clients", "desktop-compatibility-launch.test.ts": "clients",
+ "management-desktop-compatibility-routes.test.ts": "server", "management-desktop-compatibility-runtime-routes.test.ts": "server", "server-desktop-compatibility-startup.test.ts": "server", "management-desktop-compatibility-settings.test.ts": "server",
+ "optional-desktop-upstream.test.ts": "lib",
"desktop-exit-ownership.test.ts": "clients",
"desktop-host-visibility.test.ts": "clients",
"desktop-install-identity.test.ts": "clients",
diff --git a/src/claude/intercept/local-ca.ts b/src/claude/intercept/local-ca.ts
index 168e2d1c439..8e6e186743c 100644
--- a/src/claude/intercept/local-ca.ts
+++ b/src/claude/intercept/local-ca.ts
@@ -198,7 +198,9 @@ export interface AuthorityOptions {
permittedDnsNames?: readonly string[];
/** With permittedDnsNames: also exclude every IP address (default true). */
excludeAllIpAddresses?: boolean;
- }
+ /** Restrict this authority to TLS server authentication; legacy callers keep their existing scope. */
+ serverAuthOnly?: boolean;
+}
export function createCertificateAuthority(options: AuthorityOptions): LocalInterceptCa {
const validityDays = options.validityDays ?? CA_VALIDITY_DAYS;
@@ -218,6 +220,7 @@ export function createCertificateAuthority(options: AuthorityOptions): LocalInte
// keyCertSign | cRLSign
extension(OID.keyUsage, true, bitString(Uint8Array.of(0x06), 1)),
extension(OID.subjectKeyIdentifier, false, octetString(keyIdentifier(publicKey))),
+ ...(options.serverAuthOnly ? [extension(OID.extendedKeyUsage, true, sequence(objectIdentifier(OID.serverAuth)))] : []),
...(options.permittedDnsNames?.length
? [extension(OID.nameConstraints, true, nameConstraints(options.permittedDnsNames, options.excludeAllIpAddresses !== false))]
: []),
@@ -235,6 +238,10 @@ export function createLocalInterceptCa(): LocalInterceptCa {
return createCertificateAuthority({ commonName: CLAUDE_INTERCEPT_CA_COMMON_NAME });
}
+export function isServerAuthOnlyCertificate(certificate: X509Certificate): boolean {
+ return certificate.keyUsage?.length === 1 && certificate.keyUsage[0] === OID.serverAuth;
+}
+
/**
* Test hook for trust-boundary suites: mint a self-signed authority carrying an arbitrary list of
* DER-encoded Extension items so adversarial profiles still bear a valid signature. Production
diff --git a/src/cli/restart-scope.ts b/src/cli/restart-scope.ts
index 2dd14c94214..58b5264606e 100644
--- a/src/cli/restart-scope.ts
+++ b/src/cli/restart-scope.ts
@@ -173,6 +173,9 @@ export async function handleDesktopAppRestart(
+ "Quit and relaunch the desktop app manually to refresh the model picker.",
);
return result;
+ case "relaunch_context_failed":
+ log.error("Codex desktop launch options could not be preserved safely, so the app was not stopped. Review its current launch options before retrying.");
+ return result;
case "no_targets":
log.log("Codex desktop app is not running; nothing to restart.");
return result;
@@ -189,4 +192,3 @@ export async function handleDesktopAppRestart(
return result;
}
}
-
diff --git a/src/codex/desktop-app-restart.ts b/src/codex/desktop-app-restart.ts
index 6de8220ad0b..18c1e1a8470 100644
--- a/src/codex/desktop-app-restart.ts
+++ b/src/codex/desktop-app-restart.ts
@@ -89,6 +89,7 @@ export type DesktopAppRestartReason =
| "self_ancestry"
| "restart_in_flight"
| "handoff_started"
+ | "relaunch_context_failed"
| "targets_survived"
| "relaunch_failed";
@@ -263,7 +264,9 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
// graphical session variables, and after termination there is nothing to read them
// from. Ordering this wrongly works on macOS and Windows and produces a Linux app
// that cannot reach the compositor.
- const context = adapter.captureRelaunchContext(exec, install, processes);
+ let context: Record;
+ try { context = adapter.captureRelaunchContext(exec, install, processes); }
+ catch { return skipped("relaunch_context_failed"); }
const isAlive = io.isAlive ?? defaultIsAlive;
const sleep = io.sleep ?? defaultSleep;
@@ -345,4 +348,3 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
if (!handedOff) releaseDesktopRestartLock(io.lock);
}
}
-
diff --git a/src/codex/desktop-app/types.ts b/src/codex/desktop-app/types.ts
index c90862f329b..07884dd9cd9 100644
--- a/src/codex/desktop-app/types.ts
+++ b/src/codex/desktop-app/types.ts
@@ -51,6 +51,8 @@ export interface DesktopProcess {
createdAt: string;
/** Absolute executable path, used for membership and the shell predicate. */
executable: string;
+ /** Windows-only private launch context; never included in restart results or logs. */
+ commandLine?: string;
}
export interface DesktopAppAdapter {
diff --git a/src/codex/desktop-app/windows.ts b/src/codex/desktop-app/windows.ts
index c73e067e5b6..ab125fd46c8 100644
--- a/src/codex/desktop-app/windows.ts
+++ b/src/codex/desktop-app/windows.ts
@@ -13,6 +13,7 @@
import { execFileSync } from "node:child_process";
import { sep, win32 } from "node:path";
import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe } from "../../lib/windows-elevation";
+import { activateWindowsCodexCompatibility, assertWindowsCompatibilityContext, captureWindowsCompatibilityContext } from "../desktop-compatibility/windows-package-command";
import {
isUnderRoot,
type DesktopAppAdapter,
@@ -50,8 +51,7 @@ function isMemberExecutable(executable: string, root: string): boolean {
* changes between builds, so a literal AUMID would silently stop matching and
* then either do nothing or — worse — match a package we did not mean.
*/
-function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
- const script = [
+export const WINDOWS_PACKAGE_DISCOVERY_SCRIPT = [
"$ErrorActionPreference='SilentlyContinue'",
"Import-Module Appx -ErrorAction SilentlyContinue",
"$p = Get-AppxPackage -Name OpenAI.Codex",
@@ -60,12 +60,8 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
" $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
"}",
].join("; ");
- let stdout: string;
- try {
- stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], POWERSHELL_PROBE_OPTIONS);
- } catch {
- return null;
- }
+
+export function parseWindowsDesktopPackage(stdout: string): DesktopAppInstall | null {
const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
if (lines.length < 3 || lines[0] === "MISS") return null;
const [family, installLocation, aumid] = lines;
@@ -73,6 +69,16 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
return { id: family, root: installLocation, relaunch: aumid };
}
+function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
+ let stdout: string;
+ try {
+ stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], POWERSHELL_PROBE_OPTIONS);
+ } catch {
+ return null;
+ }
+ return parseWindowsDesktopPackage(stdout);
+}
+
/**
* Only `ChatGPT.exe` processes whose image lives under the discovered install
* location AND owned by the current user. The install location alone is not
@@ -100,7 +106,8 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
" if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
" $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
" if ($owner -ieq $me) {",
- " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)\"",
+ " $encoded=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_.CommandLine))",
+ " \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)`t$encoded\"",
" }",
" }",
" }",
@@ -126,7 +133,9 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
}
function parseProcessLine(line: string, root: string): DesktopProcess | null {
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(line);
+ const [listing, encoded, ...extra] = line.split("\t");
+ if (extra.length || (encoded && (encoded.length > 65_536 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)))) return null;
+ const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(listing ?? "");
if (!match) return null;
const pid = Number(match[1]);
const parentPid = Number(match[2]);
@@ -143,7 +152,8 @@ function parseProcessLine(line: string, root: string): DesktopProcess | null {
// Authoritative membership. PowerShell StartsWith already cheap-filtered, but
// that test is a string prefix and is how a sibling install would sneak in.
if (!isMemberExecutable(executable, root)) return null;
- return { pid, parentPid, createdAt, executable };
+ return { pid, parentPid, createdAt, executable,
+ ...(encoded !== undefined ? { commandLine: Buffer.from(encoded, "base64").toString("utf8") } : {}) };
}
/**
@@ -215,13 +225,16 @@ export const windowsDesktopAppAdapter: DesktopAppAdapter = {
exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(root.pid), "/T", "/F"], POWERSHELL_PROBE_OPTIONS);
},
- captureRelaunchContext(): Record {
- // The session is supplied by the shell:AppsFolder launch, so nothing needs
- // carrying forward.
- return {};
+ captureRelaunchContext(_exec, _install, processes): Record {
+ return captureWindowsCompatibilityContext(processes);
},
- relaunch(exec, install): void {
+ relaunch(exec, install, context): void {
+ if (context.codexCompatibilityPacUrl) {
+ assertWindowsCompatibilityContext(context);
+ activateWindowsCodexCompatibility(exec, install, context.codexCompatibilityPacUrl);
+ return;
+ }
// Throws on failure so the ladder reports relaunch_failed. The old code
// returned targets_survived here, which was dishonest: everything HAD died
// and it was the relaunch that failed.
diff --git a/src/codex/desktop-compatibility/certificate-service.ts b/src/codex/desktop-compatibility/certificate-service.ts
new file mode 100644
index 00000000000..289fbb2af50
--- /dev/null
+++ b/src/codex/desktop-compatibility/certificate-service.ts
@@ -0,0 +1,99 @@
+import { join } from "node:path";
+import { getConfigDir } from "../../config/paths";
+import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
+import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority, type DesktopAuthorityInspection, type StoredDesktopAuthority } from "./certificate-store";
+import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust } from "./windows-certificate-trust";
+import { acquireDesktopCertificateMutation, desktopCompatibilityRuntimeActive } from "./runtime-ownership";
+
+export interface DesktopCertificateStatus {
+ supported: boolean;
+ state: "missing" | "invalid" | "expired" | "renewal-required" | "prepared" | "trusted" | "unknown";
+ fingerprint?: string;
+ expiresAt?: number;
+ renewalDue?: boolean;
+ trust?: DesktopCertificateTrust;
+ busy: "prepare" | "trust" | "remove-trust" | "renew" | null;
+}
+
+export interface DesktopCertificateServiceIo {
+ platform?: string;
+ inspect?: () => DesktopAuthorityInspection;
+ prepare?: () => Promise;
+ load?: (allowExpiredForRemoval: boolean) => Promise;
+ renew?: (fingerprint: string) => Promise;
+ readTrust?: (authority: Extract) => Promise;
+ changeTrust?: (authority: StoredDesktopAuthority, action: "trust" | "remove") => Promise;
+ appRunning?: () => Promise;
+}
+
+export class DesktopCertificateServiceError extends Error {
+ constructor(readonly code: "unsupported" | "busy" | "not_prepared" | "fingerprint_changed" | "app_running" | "runtime_running" | "app_state_unknown" | "trust_unknown" | "trust_not_applied") {
+ super(`desktop_compatibility_${code}`); this.name = "DesktopCertificateServiceError";
+ }
+}
+
+/** This service owns setup only: no proxy listeners, app restart, account or quota writes. */
+export function createDesktopCertificateService(directory = join(getConfigDir(), "codex-desktop-compatibility"), io: DesktopCertificateServiceIo = {}) {
+ const platform = io.platform ?? process.platform;
+ const inspect = io.inspect ?? (() => inspectDesktopCompatibilityAuthority(directory));
+ const prepare = io.prepare ?? (() => ensureDesktopCompatibilityAuthority({ directory }));
+ const load = io.load ?? (allowExpired => loadDesktopCompatibilityAuthority({ directory }, allowExpired));
+ const renew = io.renew ?? (fingerprint => renewDesktopCompatibilityAuthority({ directory }, fingerprint));
+ const readTrust = io.readTrust ?? (value => inspectWindowsCertificateTrust(value.certPem, value.fingerprint));
+ const changeTrust = io.changeTrust ?? ((authority, action) => createWindowsCertificateTrust(authority, authority.fingerprint)[action]());
+ const appRunning = io.appRunning ?? (async () => {
+ const install = windowsDesktopAppAdapter.discover(windowsDefaultExec);
+ if (!install) return null; // Unknown installation is not proof that no app uses the authority.
+ const processes = windowsDesktopAppAdapter.listProcesses(windowsDefaultExec, install);
+ return processes === null ? null : processes.length > 0;
+ });
+ let busy: DesktopCertificateStatus["busy"] = null;
+ async function status(): Promise {
+ if (platform !== "win32") return { supported: false, state: "missing", busy };
+ const stored = inspect();
+ if (!("fingerprint" in stored)) return { supported: true, state: stored.status, busy };
+ const trusted = await readTrust(stored).catch(() => "unknown" as const);
+ return { supported: true, state: stored.status === "expired" ? "expired"
+ : stored.status === "renewal-required" ? "renewal-required"
+ : trusted === "trusted" ? "trusted" : trusted === "not-trusted" ? "prepared" : "unknown",
+ fingerprint: stored.fingerprint, expiresAt: stored.expiresAt, renewalDue: stored.renewalDue, trust: trusted, busy };
+ }
+ async function action(kind: NonNullable, expectedFingerprint?: string): Promise {
+ if (platform !== "win32") throw new DesktopCertificateServiceError("unsupported");
+ if (busy !== null) throw new DesktopCertificateServiceError("busy");
+ if (desktopCompatibilityRuntimeActive()) throw new DesktopCertificateServiceError("runtime_running");
+ let release: () => void;
+ try { release = acquireDesktopCertificateMutation(); } catch { throw new DesktopCertificateServiceError("busy"); }
+ busy = kind;
+ try {
+ if (kind !== "prepare") {
+ const before = inspect();
+ if (!("fingerprint" in before)) throw new DesktopCertificateServiceError("not_prepared");
+ if (before.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
+ if (kind === "remove-trust" || kind === "renew") {
+ const running = await appRunning();
+ if (running === null) throw new DesktopCertificateServiceError("app_state_unknown");
+ if (running) throw new DesktopCertificateServiceError("app_running");
+ }
+ }
+ const authority = kind === "prepare" ? await prepare() : await load(kind !== "trust");
+ if (kind !== "prepare") {
+ if (authority.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
+ const current = inspect();
+ if (!("fingerprint" in current) || current.fingerprint !== authority.fingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
+ const result = await changeTrust(authority, kind === "trust" ? "trust" : "remove");
+ if (result === "unknown") throw new DesktopCertificateServiceError("trust_unknown");
+ if (result !== (kind === "trust" ? "trusted" : "not-trusted")) throw new DesktopCertificateServiceError("trust_not_applied");
+ // Never discard the only removable old identity until OS trust removal is proven.
+ // Publication failure leaves its protected envelope intact for an explicit retry.
+ if (kind === "renew") await renew(authority.fingerprint);
+ }
+ busy = null;
+ return status();
+ } finally { busy = null; release(); }
+ }
+ return { status, prepare: () => action("prepare"), trust: (fingerprint: string) => action("trust", fingerprint),
+ removeTrust: (fingerprint: string) => action("remove-trust", fingerprint), renew: (fingerprint: string) => action("renew", fingerprint) };
+}
+
+export type DesktopCertificateService = ReturnType;
diff --git a/src/codex/desktop-compatibility/certificate-store.ts b/src/codex/desktop-compatibility/certificate-store.ts
new file mode 100644
index 00000000000..26af38df442
--- /dev/null
+++ b/src/codex/desktop-compatibility/certificate-store.ts
@@ -0,0 +1,193 @@
+import { createHash, createPrivateKey, createPublicKey, randomUUID, X509Certificate } from "node:crypto";
+import { closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
+import { isAbsolute, join } from "node:path";
+import { createCertificateAuthority, isServerAuthOnlyCertificate, type LocalInterceptCa } from "../../claude/intercept/local-ca";
+import { withClientLifecycle } from "../../client/lifecycle-lock";
+import { hardenSecretDirAsync, hardenSecretPathAsync } from "../../lib/windows-secret-acl";
+import { windowsAuthorityKeyProtection, type AuthorityKeyProtection } from "./windows-key-protection";
+
+const POLICY = "codex-desktop-chatgpt-only/v1";
+const FILE = "authority.json";
+const MAX_FILE = 131_072;
+const VALIDITY_DAYS = 30;
+const DAY = 86_400_000;
+const digest = (value: string) => createHash("sha256").update(value).digest("hex");
+
+export class DesktopAuthorityError extends Error {
+ constructor(readonly code: "unsafe_path" | "unreadable" | "expired" | "renewal_required" | "protection_failed" | "fingerprint_changed") {
+ super(`desktop_compatibility_authority_${code}`); this.name = "DesktopAuthorityError";
+ }
+}
+
+export interface DesktopAuthorityStoreOptions {
+ /** Explicit feature-owned directory; never an OS trust store or existing Claude CA. */
+ directory: string;
+ now?: () => number;
+ /** Test seam; production always uses Windows CurrentUser DPAPI. */
+ protection?: AuthorityKeyProtection;
+}
+
+export interface StoredDesktopAuthority {
+ authority: LocalInterceptCa;
+ commonName: string;
+ fingerprint: string;
+ expiresAt: number;
+ renewalDue: boolean;
+ reused: boolean;
+}
+
+export type DesktopAuthorityInspection =
+ | { status: "missing" | "invalid" }
+ | { status: "present" | "expired" | "renewal-required"; certPem: string; fingerprint: string; expiresAt: number; renewalDue: boolean };
+
+/** Read-only public metadata for status: no key decryption, ACL write, lock or generation. */
+export function inspectDesktopCompatibilityAuthority(directory: string, now = Date.now()): DesktopAuthorityInspection {
+ if (!isAbsolute(directory) || !Number.isFinite(now)) return { status: "invalid" };
+ const path = join(directory, FILE);
+ try {
+ if (!pathPresent(directory)) return { status: "missing" };
+ assertPath(directory, true);
+ if (!pathPresent(path)) return { status: "missing" };
+ assertPath(path, false);
+ const saved = JSON.parse(readFileSync(path, "utf8"));
+ if (saved.version !== 1 || saved.protection !== "windows-current-user-dpapi" || typeof saved.certPem !== "string"
+ || typeof saved.sealed !== "string" || !/^[A-Za-z0-9+/]+={0,2}$/.test(saved.sealed) || saved.sealed.length > MAX_FILE) return { status: "invalid" };
+ const certificate = new X509Certificate(saved.certPem), expiresAt = Date.parse(certificate.validTo);
+ if (!certificate.ca || !Number.isFinite(expiresAt)) return { status: "invalid" };
+ return { status: expiresAt <= now ? "expired" : isServerAuthOnlyCertificate(certificate) ? "present" : "renewal-required", certPem: saved.certPem,
+ fingerprint: certificate.fingerprint256.replaceAll(":", ""), expiresAt, renewalDue: expiresAt - now <= 7 * DAY };
+ } catch { return { status: "invalid" }; }
+}
+
+function assertPath(path: string, directory: boolean): void {
+ const stat = lstatSync(path);
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > MAX_FILE)) {
+ throw new DesktopAuthorityError("unsafe_path");
+ }
+}
+
+function pathPresent(path: string): boolean {
+ try { lstatSync(path); return true; }
+ catch (error) {
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
+ throw new DesktopAuthorityError("unsafe_path");
+ }
+}
+
+function validatedAuthority(certPem: string, keyPem: string, commonName: string, now: number, reused: boolean, allowExpired = false): StoredDesktopAuthority {
+ try {
+ const certificate = new X509Certificate(certPem);
+ const privateKey = createPrivateKey(keyPem), publicKey = createPublicKey(keyPem);
+ if (!certificate.ca || !certificate.checkPrivateKey(privateKey) || !certificate.verify(publicKey)
+ || certificate.subject !== certificate.issuer
+ || !commonName.startsWith("OpenCodex Codex Desktop ") || !certificate.subject.split("\n").includes(`CN=${commonName}`)) {
+ throw new Error("invalid");
+ }
+ const expiresAt = Date.parse(certificate.validTo), startsAt = Date.parse(certificate.validFrom);
+ if (!Number.isFinite(expiresAt) || !Number.isFinite(startsAt) || startsAt > now
+ || expiresAt - startsAt > (VALIDITY_DAYS + 1) * DAY) throw new Error("invalid");
+ if (expiresAt <= now && !allowExpired) throw new DesktopAuthorityError("expired");
+ if (!allowExpired && !isServerAuthOnlyCertificate(certificate)) throw new DesktopAuthorityError("renewal_required");
+ return { authority: { certPem, keyPem, privateKey, publicKey }, commonName,
+ fingerprint: certificate.fingerprint256.replaceAll(":", ""), expiresAt,
+ renewalDue: expiresAt - now <= 7 * DAY, reused };
+ } catch (error) {
+ if (error instanceof DesktopAuthorityError) throw error;
+ throw new DesktopAuthorityError("unreadable");
+ }
+}
+
+async function readAuthority(path: string, protection: AuthorityKeyProtection, now: number, allowExpired = false): Promise {
+ assertPath(path, false);
+ await hardenSecretPathAsync(path, { required: true });
+ assertPath(path, false);
+ let cleartext: Uint8Array | undefined;
+ try {
+ const saved = JSON.parse(readFileSync(path, "utf8"));
+ if (saved.version !== 1 || saved.protection !== "windows-current-user-dpapi"
+ || typeof saved.certPem !== "string" || typeof saved.sealed !== "string"
+ || !/^[A-Za-z0-9+/]+={0,2}$/.test(saved.sealed) || saved.sealed.length > MAX_FILE) throw new Error("invalid");
+ cleartext = await protection.unprotect(Buffer.from(saved.sealed, "base64"));
+ const secret = JSON.parse(Buffer.from(cleartext).toString("utf8"));
+ if (secret.policy !== POLICY || secret.certSha256 !== digest(saved.certPem)
+ || typeof secret.keyPem !== "string" || typeof secret.commonName !== "string") throw new Error("invalid");
+ return validatedAuthority(saved.certPem, secret.keyPem, secret.commonName, now, true, allowExpired);
+ } catch (error) {
+ if (error instanceof DesktopAuthorityError) throw error;
+ // A corrupt or foreign-user key never silently regenerates a new root or trust prompt.
+ throw new DesktopAuthorityError("unreadable");
+ } finally { cleartext?.fill(0); }
+}
+
+/** Loads only existing state. Expired or legacy-purpose keys may be loaded solely for trust removal/renewal. */
+export async function loadDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions, allowExpiredForRemoval = false): Promise {
+ if (!isAbsolute(options.directory)) throw new DesktopAuthorityError("unsafe_path");
+ const now = options.now?.() ?? Date.now();
+ if (!Number.isFinite(now)) throw new DesktopAuthorityError("unreadable");
+ assertPath(options.directory, true);
+ const path = join(options.directory, FILE);
+ if (!pathPresent(path)) throw new DesktopAuthorityError("unreadable");
+ return readAuthority(path, options.protection ?? windowsAuthorityKeyProtection, now, allowExpiredForRemoval);
+}
+
+/** Reuses one user-protected root across restarts; never installs, rotates or removes trust. */
+export async function ensureDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions): Promise {
+ return publishAuthority(options);
+}
+
+/** Deliberate replacement after the caller has removed OS trust and stopped consumers. */
+export async function renewDesktopCompatibilityAuthority(options: DesktopAuthorityStoreOptions, expectedFingerprint: string): Promise {
+ if (!/^[A-F0-9]{64}$/.test(expectedFingerprint)) throw new DesktopAuthorityError("fingerprint_changed");
+ return publishAuthority(options, expectedFingerprint);
+}
+
+async function publishAuthority(options: DesktopAuthorityStoreOptions, expectedFingerprint?: string): Promise {
+ if (!isAbsolute(options.directory)) throw new DesktopAuthorityError("unsafe_path");
+ if (!options.protection && process.platform !== "win32") throw new Error("desktop_compatibility_windows_required");
+ const now = options.now?.() ?? Date.now();
+ if (!Number.isFinite(now)) throw new DesktopAuthorityError("unreadable");
+ const protection = options.protection ?? windowsAuthorityKeyProtection;
+ mkdirSync(options.directory, { recursive: true, mode: 0o700 });
+ assertPath(options.directory, true);
+ await hardenSecretDirAsync(options.directory, { required: true });
+ return withClientLifecycle(async () => {
+ assertPath(options.directory, true);
+ const path = join(options.directory, FILE);
+ let original: string | undefined;
+ if (expectedFingerprint !== undefined) {
+ if (!pathPresent(path)) throw new DesktopAuthorityError("fingerprint_changed");
+ const previous = await readAuthority(path, protection, now, true);
+ if (previous.fingerprint !== expectedFingerprint) throw new DesktopAuthorityError("fingerprint_changed");
+ original = readFileSync(path, "utf8");
+ } else if (pathPresent(path)) return readAuthority(path, protection, now);
+ const commonName = `OpenCodex Codex Desktop ${randomUUID()}`;
+ const authority = createCertificateAuthority({ commonName, validityDays: VALIDITY_DAYS,
+ permittedDnsNames: ["chatgpt.com"], excludeAllIpAddresses: true, serverAuthOnly: true });
+ const cleartext = Buffer.from(JSON.stringify({ policy: POLICY, certSha256: digest(authority.certPem), commonName, keyPem: authority.keyPem }));
+ let sealed: Uint8Array;
+ try { sealed = await protection.protect(cleartext); }
+ catch { throw new DesktopAuthorityError("protection_failed"); }
+ finally { cleartext.fill(0); }
+ if (sealed.byteLength === 0 || sealed.byteLength > 65_536) throw new DesktopAuthorityError("protection_failed");
+ const contents = JSON.stringify({ version: 1, protection: "windows-current-user-dpapi", certPem: authority.certPem,
+ sealed: Buffer.from(sealed).toString("base64") });
+ const temporary = join(options.directory, `authority-${randomUUID()}.tmp`);
+ let created = false;
+ try {
+ const fd = openSync(temporary, "wx", 0o600); created = true;
+ try { writeFileSync(fd, contents); fsyncSync(fd); } finally { closeSync(fd); }
+ await hardenSecretPathAsync(temporary, { required: true });
+ assertPath(temporary, false); assertPath(options.directory, true);
+ // Verify the new encrypted envelope before replacing the only recovery source.
+ await readAuthority(temporary, protection, now);
+ if (original !== undefined) {
+ assertPath(path, false);
+ if (readFileSync(path, "utf8") !== original) throw new DesktopAuthorityError("fingerprint_changed");
+ } else if (pathPresent(path)) throw new DesktopAuthorityError("unsafe_path");
+ renameSync(temporary, path); created = false;
+ // Read back the actual persisted pair before reporting a successful setup.
+ const persisted = await readAuthority(path, protection, now);
+ return { ...persisted, reused: false };
+ } finally { if (created) unlinkSync(temporary); }
+ }, { lockPath: join(options.directory, "authority-publication.sqlite") });
+}
diff --git a/src/codex/desktop-compatibility/connection-store.ts b/src/codex/desktop-compatibility/connection-store.ts
new file mode 100644
index 00000000000..d6512d8bd71
--- /dev/null
+++ b/src/codex/desktop-compatibility/connection-store.ts
@@ -0,0 +1,87 @@
+import { randomUUID } from "node:crypto";
+import { closeSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
+import { isAbsolute, join } from "node:path";
+import { withClientLifecycle } from "../../client/lifecycle-lock";
+import { hardenSecretDirAsync, hardenSecretPathAsync } from "../../lib/windows-secret-acl";
+
+export interface DesktopConnectionIdentity {
+ version: 1;
+ id: string;
+ connectPort: number;
+ pacPort: number;
+}
+export interface DesktopConnectionStore {
+ read(): DesktopConnectionIdentity | null;
+ publish(value: DesktopConnectionIdentity): Promise;
+}
+const FILE = "connection.json";
+const fail = (reason: "invalid" | "changed" | "cleanup_required", cause?: unknown) => new Error(`desktop_compatibility_connection_${reason}`, { cause });
+const port = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 1024 && Number(value) <= 65535;
+function validate(value: unknown): DesktopConnectionIdentity {
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw fail("invalid");
+ const row = value as Record;
+ if (Object.keys(row).length !== 4 || row.version !== 1 || typeof row.id !== "string"
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(row.id)
+ || !port(row.connectPort) || !port(row.pacPort) || row.connectPort === row.pacPort) throw fail("invalid");
+ return { version: 1, id: row.id, connectPort: row.connectPort, pacPort: row.pacPort };
+}
+function assertPath(path: string, directory: boolean): void {
+ const stat = lstatSync(path);
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > 4096)) throw fail("invalid");
+}
+function present(path: string): boolean {
+ try { lstatSync(path); return true; }
+ catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false; throw fail("invalid"); }
+}
+function same(left: DesktopConnectionIdentity, right: DesktopConnectionIdentity): boolean {
+ return left.version === right.version && left.id === right.id && left.connectPort === right.connectPort && left.pacPort === right.pacPort;
+}
+
+/** Public endpoint identity only. No token, account, private key or expiry is persisted here. */
+export function createDesktopConnectionStore(directory: string): DesktopConnectionStore {
+ if (!isAbsolute(directory)) throw fail("invalid");
+ const path = join(directory, FILE);
+ const read = (): DesktopConnectionIdentity | null => {
+ try {
+ if (!present(directory)) return null;
+ assertPath(directory, true);
+ if (!present(path)) return null;
+ assertPath(path, false);
+ return validate(JSON.parse(readFileSync(path, "utf8")));
+ } catch { throw fail("invalid"); }
+ };
+ const verifyExisting = (value: DesktopConnectionIdentity): DesktopConnectionIdentity | null => {
+ const existing = read();
+ if (existing && !same(existing, value)) throw fail("changed");
+ return existing;
+ };
+ return { read, async publish(input) {
+ const value = validate(input);
+ const existing = verifyExisting(value); if (existing) return existing;
+ // A trusted authority must already own this directory. Never create a replacement home.
+ assertPath(directory, true); await hardenSecretDirAsync(directory, { required: true });
+ return withClientLifecycle(async () => {
+ const raced = verifyExisting(value); if (raced) return raced;
+ const temporary = join(directory, `connection-${randomUUID()}.tmp`);
+ let created = false;
+ try {
+ const fd = openSync(temporary, "wx", 0o600); created = true;
+ try { writeFileSync(fd, JSON.stringify(value)); fsyncSync(fd); } finally { closeSync(fd); }
+ await hardenSecretPathAsync(temporary, { required: true });
+ assertPath(directory, true); assertPath(temporary, false);
+ // Create-only publication cannot overwrite another process's endpoint identity.
+ try { linkSync(temporary, path); }
+ catch (error) {
+ const winner = verifyExisting(value); if (!winner) throw error;
+ }
+ unlinkSync(temporary); created = false;
+ const published = verifyExisting(value); if (!published) throw fail("changed");
+ return published;
+ } catch (error) {
+ try { if (created && present(temporary)) unlinkSync(temporary); }
+ catch (cleanupError) { throw fail("cleanup_required", new AggregateError([error, cleanupError], "Endpoint publication and cleanup failed")); }
+ throw error;
+ }
+ }, { lockPath: join(directory, "connection-publication.sqlite") });
+ } };
+}
diff --git a/src/codex/desktop-compatibility/installed-build.ts b/src/codex/desktop-compatibility/installed-build.ts
new file mode 100644
index 00000000000..36dbf3bc5b5
--- /dev/null
+++ b/src/codex/desktop-compatibility/installed-build.ts
@@ -0,0 +1,55 @@
+import { execFile } from "node:child_process";
+import { win32 } from "node:path";
+import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
+import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
+import { parseWindowsDesktopPackage, WINDOWS_PACKAGE_DISCOVERY_SCRIPT } from "../desktop-app/windows";
+import type { DesktopAppInstall } from "../desktop-app/types";
+
+export const DESKTOP_COMPATIBILITY_ASSESSED_VERSION = "26.924.2738.0";
+export const DESKTOP_COMPATIBILITY_ASSESSED_FAMILY = "OpenAI.Codex_2p2nqsd0c76g0";
+
+function matchesAssessedPackage(install: DesktopAppInstall): boolean {
+ const name = win32.basename(install.root);
+ const parts = /^OpenAI\.Codex_(\d+\.\d+\.\d+\.\d+)_(x64|arm64|x86|neutral)_([A-Za-z0-9.-]*)_([A-Za-z0-9]+)$/.exec(name);
+ return install.id === DESKTOP_COMPATIBILITY_ASSESSED_FAMILY && install.relaunch === `${install.id}!App`
+ && !!parts && parts[1] === DESKTOP_COMPATIBILITY_ASSESSED_VERSION && `OpenAI.Codex_${parts[4]}` === install.id;
+}
+
+/** Resolve only after the exact asynchronous child has closed, including abort/timeout. */
+function queryInstalledPackage(signal: AbortSignal): Promise {
+ if (process.platform !== "win32" || isTestHomeGuardArmed() || signal.aborted) return Promise.resolve(null);
+ return new Promise(resolve => {
+ let completed = false, closed = false, result: DesktopAppInstall | null = null;
+ const finish = () => { if (completed && closed) { signal.removeEventListener("abort", abort); resolve(result); } };
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], {
+ timeout: 10000, maxBuffer: 65536, encoding: "utf8", windowsHide: true,
+ }, (error, stdout) => { result = error ? null : parseWindowsDesktopPackage(stdout); completed = true; finish(); });
+ const abort = () => { try { child.kill(); } catch { /* Keep ownership until execFile reports close. */ } };
+ child.once("close", () => { closed = true; finish(); });
+ signal.addEventListener("abort", abort, { once: true });
+ if (signal.aborted) abort();
+ });
+}
+
+/** Coalesce concurrent fresh probes; never cache a positive verdict across requests. */
+export function createInstalledBuildProbe(query: (signal: AbortSignal) => Promise = queryInstalledPackage) {
+ let pending: Promise | null = null, controller: AbortController | null = null, closed = false;
+ return {
+ check(): Promise {
+ if (closed) return Promise.resolve(false);
+ if (pending) return pending;
+ const current = new AbortController(); controller = current;
+ pending = Promise.resolve().then(() => current.signal.aborted ? null : query(current.signal))
+ .then(install => !closed && !current.signal.aborted && !!install
+ && matchesAssessedPackage(install))
+ .catch(() => false).finally(() => { pending = null; controller = null; });
+ return pending;
+ },
+ async close(): Promise { closed = true; controller?.abort(); await pending; },
+ };
+}
+
+export async function isAssessedDesktopInstalled(): Promise {
+ const probe = createInstalledBuildProbe();
+ try { return await probe.check(); } finally { await probe.close(); }
+}
diff --git a/src/codex/desktop-compatibility/json-body.ts b/src/codex/desktop-compatibility/json-body.ts
new file mode 100644
index 00000000000..d51e38ca0b5
--- /dev/null
+++ b/src/codex/desktop-compatibility/json-body.ts
@@ -0,0 +1,29 @@
+/** Buffer small JSON only; large bodies pass through without collecting the remainder. */
+export async function boundedJsonBody(body: ReadableStream | null, maxBytes = 262144): Promise<{ bytes: Buffer } | { stream: ReadableStream }> {
+ if (!body) return { bytes: Buffer.alloc(0) };
+ const reader = body.getReader();
+ const chunks: Uint8Array[] = [];
+ let count = 0;
+ try {
+ while (true) {
+ const next = await reader.read();
+ if (next.done) { reader.releaseLock(); return { bytes: Buffer.concat(chunks, count) }; }
+ chunks.push(next.value); count += next.value.byteLength;
+ if (count <= maxBytes && chunks.length < 1024) continue;
+ let index = 0;
+ return { stream: new ReadableStream({
+ async pull(controller) {
+ if (index < chunks.length) {
+ const value = chunks[index]!; chunks[index++] = new Uint8Array(0); controller.enqueue(value); return;
+ }
+ try {
+ const part = await reader.read();
+ if (part.done) { reader.releaseLock(); controller.close(); }
+ else controller.enqueue(part.value);
+ } catch (error) { reader.releaseLock(); controller.error(error); }
+ },
+ async cancel(reason) { try { await reader.cancel(reason); } finally { reader.releaseLock(); } },
+ }) };
+ }
+ } catch (error) { reader.releaseLock(); throw error; }
+}
diff --git a/src/codex/desktop-compatibility/native-identity.ts b/src/codex/desktop-compatibility/native-identity.ts
new file mode 100644
index 00000000000..e3f97d59e86
--- /dev/null
+++ b/src/codex/desktop-compatibility/native-identity.ts
@@ -0,0 +1,62 @@
+import { readFileSync, statSync, type BigIntStats } from 'node:fs';
+import { createHash, randomUUID } from 'node:crypto';
+import type { UsageIdentity } from './usage-controller';
+
+type Credential = { identity: UsageIdentity; accessToken: string };
+const equal = (a: UsageIdentity, b: UsageIdentity) => a.id === b.id && a.userId === b.userId && a.plan === b.plan
+ && a.credentialGeneration === b.credentialGeneration;
+
+/** Local native credentials remain in memory and are never returned by the public reader. */
+export function createNativeIdentityReader(authPath: string, upstreamFetch: typeof fetch = fetch) {
+ const readerId = randomUUID(); let generation = 0, previousSnapshot: string | undefined;
+ const stamp = (value: BigIntStats) =>
+ `${value.dev}:${value.ino}:${value.size}:${value.mtimeNs}:${value.ctimeNs}`;
+ const readCredential = (): Credential => {
+ const before = statSync(authPath, { bigint: true });
+ if (before.size > 1048576n) throw new Error('Unexpected native auth size');
+ const text = readFileSync(authPath, 'utf8'), after = statSync(authPath, { bigint: true });
+ if (stamp(before) !== stamp(after)) throw new Error('Native credentials changed during read');
+ const snapshot = `${stamp(after)}:${createHash('sha256').update(text).digest('hex')}`;
+ if (snapshot !== previousSnapshot) { previousSnapshot = snapshot; generation++; }
+ const auth = JSON.parse(text);
+ if (auth.auth_mode !== 'chatgpt' || typeof auth.tokens?.id_token !== 'string'
+ || typeof auth.tokens?.access_token !== 'string' || !auth.tokens.access_token
+ || typeof auth.tokens?.account_id !== 'string') throw new Error('Native login required');
+ // These local claims are only a binding hint. Activation additionally verifies upstream.
+ const claims = JSON.parse(Buffer.from(auth.tokens.id_token.split('.')[1], 'base64url').toString('utf8'))['https://api.openai.com/auth'];
+ if (!claims || claims.chatgpt_account_id !== auth.tokens.account_id
+ || typeof claims.chatgpt_user_id !== 'string' || !claims.chatgpt_user_id
+ || !['plus', 'pro'].includes(claims.chatgpt_plan_type)) throw new Error('Unsupported identity');
+ return { identity: { id: auth.tokens.account_id, userId: claims.chatgpt_user_id,
+ plan: claims.chatgpt_plan_type, structure: 'personal', credentialGeneration: `${readerId}:${generation}` }, accessToken: auth.tokens.access_token };
+ };
+ const readCurrentIdentity = async (): Promise => {
+ try { return readCredential().identity; } catch { return null; }
+ };
+ const verifyFreshIdentity = async (): Promise => {
+ try {
+ const before = readCredential();
+ const response = await upstreamFetch('https://chatgpt.com/backend-api/wham/usage', {
+ headers: { authorization: 'Bearer ' + before.accessToken, 'ChatGPT-Account-ID': before.identity.id },
+ redirect: 'manual', signal: AbortSignal.timeout(10000),
+ });
+ if (response.status !== 200 || !response.body) { await response.body?.cancel(); return null; }
+ const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0;
+ try {
+ for (;;) {
+ const part = await reader.read(); if (part.done) break;
+ size += part.value.length;
+ if (size > 65536) { await reader.cancel(); return null; }
+ chunks.push(part.value);
+ }
+ } finally { reader.releaseLock(); }
+ const usage = JSON.parse(Buffer.concat(chunks).toString('utf8'));
+ const after = readCredential();
+ if (!equal(before.identity, after.identity) || before.accessToken !== after.accessToken
+ || usage.account_id !== before.identity.id || usage.user_id !== before.identity.userId
+ || usage.plan_type !== before.identity.plan) return null;
+ return before.identity;
+ } catch { return null; }
+ };
+ return { readCurrentIdentity, verifyFreshIdentity };
+}
diff --git a/src/codex/desktop-compatibility/relay-listener.ts b/src/codex/desktop-compatibility/relay-listener.ts
new file mode 100644
index 00000000000..72fb1e3a2ed
--- /dev/null
+++ b/src/codex/desktop-compatibility/relay-listener.ts
@@ -0,0 +1,93 @@
+import { createServer } from "node:https";
+import { Readable, type Duplex } from "node:stream";
+import type { PemKeyPair } from "../../claude/intercept/local-ca";
+import { forwardHeadersForUpstream } from "../../claude/intercept/listener";
+import { dialDesktopUpstream, type DesktopTunnelOptions } from "../../lib/desktop-upstream-tunnel";
+
+const ORIGIN = "https://chatgpt.com";
+const STRIP = new Set(["connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length", "alt-svc", "proxy-authenticate"]);
+export interface DesktopRelayOptions {
+ leaf: PemKeyPair;
+ fetchImpl: typeof fetch;
+ /** Test-only TLS peer; the production destination is fixed, never taken from a request. */
+ websocketPeer?: { host: string; port: number; ca: string };
+ websocketTransport?: Pick;
+}
+
+/** Transparent HTTP and raw upgraded-socket relay. Only fetchImpl owns usage policy. */
+export async function startDesktopRelay(options: DesktopRelayOptions) {
+ const sockets = new Set(), requests = new Set();
+ const server = createServer({ cert: options.leaf.certPem, key: options.leaf.keyPem, ALPNProtocols: ["http/1.1"] });
+ server.on("connection", socket => { sockets.add(socket); socket.once("close", () => sockets.delete(socket)); });
+ const valid = (host: string | undefined, path: string | undefined) =>
+ (host?.toLowerCase() === "chatgpt.com" || host?.toLowerCase() === "chatgpt.com:443") && !!path && path.startsWith("/") && !path.startsWith("//");
+ server.on("request", (req, res) => {
+ if (!valid(req.headers.host, req.url)) { res.writeHead(421); res.end(); return; }
+ const abort = new AbortController(); requests.add(abort);
+ const finish = () => { requests.delete(abort); abort.abort(); };
+ res.once("close", finish); req.once("error", finish);
+ void (async () => {
+ const headers = new Headers();
+ for (let i = 0; i < req.rawHeaders.length; i += 2) headers.append(req.rawHeaders[i]!, req.rawHeaders[i + 1]!);
+ const method = req.method ?? "GET";
+ const response = await options.fetchImpl(ORIGIN + req.url, {
+ method, headers: forwardHeadersForUpstream(headers), redirect: "manual", signal: abort.signal,
+ // Node's and Bun's declarations disagree on BYOB overloads; both implement Web streams.
+ body: method === "GET" || method === "HEAD" ? undefined : Readable.toWeb(req) as unknown as ReadableStream,
+ // @ts-expect-error Node-compatible streaming fetch requires half duplex.
+ duplex: "half",
+ });
+ if (res.destroyed) { await response.body?.cancel(); return; }
+ const output: Record = {};
+ response.headers.forEach((value, key) => { if (!STRIP.has(key) && key !== "set-cookie") output[key] = value; });
+ const cookies = response.headers.getSetCookie(); if (cookies.length) output["set-cookie"] = cookies;
+ res.writeHead(response.status, output);
+ if (!response.body || method === "HEAD") { await response.body?.cancel(); res.end(); return; }
+ const body = Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream);
+ body.once("error", () => res.destroy()); res.once("close", () => body.destroy()); body.pipe(res);
+ })().catch(() => {
+ if (res.headersSent) res.destroy(); else { res.writeHead(502, { "Content-Length": "0" }); res.end(); }
+ });
+ });
+ server.on("upgrade", (req, client, head) => {
+ if (!valid(req.headers.host, req.url)) { client.end("HTTP/1.1 421 Misdirected Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); return; }
+ const peer = options.websocketPeer;
+ const abort = new AbortController(); requests.add(abort);
+ client.once("close", () => { requests.delete(abort); abort.abort(); });
+ client.once("error", () => abort.abort());
+ void dialDesktopUpstream({ ...options.websocketTransport, ...(peer ? { target: { host: peer.host, port: peer.port }, ca: peer.ca } : {}), signal: abort.signal }).then(upstream => {
+ requests.delete(abort);
+ if (client.destroyed || abort.signal.aborted) { upstream.destroy(); return; }
+ sockets.add(upstream);
+ upstream.once("error", () => client.destroy());
+ upstream.once("close", () => { sockets.delete(upstream); client.destroy(); });
+ client.once("close", () => upstream.destroy());
+ const lines = [`${req.method ?? "GET"} ${req.url} HTTP/1.1`];
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
+ if (!/^proxy-(?:authorization|connection)$/i.test(req.rawHeaders[i]!)) lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
+ }
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
+ if (head.length) upstream.write(head);
+ client.pipe(upstream).pipe(client);
+ }).catch(() => {
+ requests.delete(abort);
+ if (!client.destroyed) client.end("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+ });
+ });
+ try {
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
+ });
+ } catch (error) { server.close(); throw error; }
+ const address = server.address();
+ if (!address || typeof address === "string") throw new Error("desktop_compatibility_listener_unavailable");
+ let closing: Promise | null = null;
+ return { port: address.port, close() {
+ if (closing) return closing;
+ for (const abort of requests) abort.abort();
+ for (const socket of sockets) socket.destroy();
+ closing = new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
+ server.closeAllConnections(); return closing;
+ } };
+}
diff --git a/src/codex/desktop-compatibility/routing-binding.ts b/src/codex/desktop-compatibility/routing-binding.ts
new file mode 100644
index 00000000000..f29ef80fd3f
--- /dev/null
+++ b/src/codex/desktop-compatibility/routing-binding.ts
@@ -0,0 +1,10 @@
+import type { OcxConfig } from "../../types";
+
+export interface NativeCompatibilityOwner { hostname: string; port: number; loopbackPort?: number; config: OcxConfig }
+let owner: NativeCompatibilityOwner | null = null;
+/** Bound sockets, not persisted desired ports, identify this process's native routing target. */
+export function bindNativeCompatibilityOwner(value: NativeCompatibilityOwner): () => void {
+ owner = value;
+ return () => { if (owner === value) owner = null; };
+}
+export function nativeCompatibilityOwner(): NativeCompatibilityOwner | null { return owner; }
diff --git a/src/codex/desktop-compatibility/routing-preflight.ts b/src/codex/desktop-compatibility/routing-preflight.ts
new file mode 100644
index 00000000000..3e2f16df774
--- /dev/null
+++ b/src/codex/desktop-compatibility/routing-preflight.ts
@@ -0,0 +1,69 @@
+import { lstatSync, readFileSync } from "node:fs";
+import { createHash } from "node:crypto";
+import { join } from "node:path";
+import type { OcxConfig } from "../../types";
+import { nativeCompatibilityOwner, type NativeCompatibilityOwner } from "./routing-binding";
+
+const record = (value: unknown): value is Record => !!value && typeof value === "object" && !Array.isArray(value);
+// These are routing inputs, not proof of a conversation's final provider. Their
+// changes revoke the observed context even if the loopback URL stays the same.
+const ROUTING_KEYS = ["providers", "defaultProvider", "defaultModelAliases", "customModels", "combos", "routingProfiles",
+ "subagentModelFallback", "subagentModelFallbackByModel", "injectionModel", "compactionRouting", "compactionRecovery",
+ "blockedModelRedirects", "shadowCallIntercept", "protocols", "memoryModels"] as const satisfies readonly (keyof OcxConfig)[];
+function routingDigest(text: string, owner: NativeCompatibilityOwner): string {
+ const native = Bun.TOML.parse(text) as Record;
+ // Desktop refreshes MCP endpoints after launch. Tool transport metadata and TOML
+ // formatting do not select the model transport. Keep all other (including unknown)
+ // native fields bound, rather than an allowlist that could miss a new routing key.
+ const { mcp_servers: _mcpServers, ...nativeContext } = native;
+ const projection = Object.fromEntries(ROUTING_KEYS.map(key => [key, owner.config[key]]));
+ const serialized = JSON.stringify([nativeContext, owner.hostname, owner.port, owner.loopbackPort, projection], (_key, value) =>
+ record(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value);
+ if (Buffer.byteLength(serialized) > 1048576) throw new Error("Routing snapshot too large");
+ return createHash("sha256").update(serialized).digest("hex");
+}
+/** Validates the effective root/profile routing only; it does not prove a thread's selected provider. */
+export function matchesNativeCompatibilityRouting(text: string, owner: NativeCompatibilityOwner | null): boolean {
+ if (!owner || owner.config.codexDesktopAuthless === true || owner.config.runtimeRole === "client") return false;
+ try {
+ const root: unknown = Bun.TOML.parse(text);
+ if (!record(root)) return false;
+ let effective = root;
+ if (root.profile !== undefined) {
+ if (typeof root.profile !== "string" || !record(root.profiles)) return false;
+ const profile = root.profiles[root.profile];
+ if (!record(profile)) return false;
+ effective = { ...root, ...profile };
+ }
+ if ((effective.model_provider ?? "openai") !== "openai" || effective.forced_login_method === "api") return false;
+ if (typeof effective.openai_base_url !== "string") return false;
+ const url = new URL(effective.openai_base_url);
+ if (url.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password
+ || url.search || url.hash || !["/v1", "/v1/"].includes(url.pathname) || !url.port) return false;
+ const port = Number(url.port), matchesPort = (value: number | undefined) => Number.isInteger(value) && Number(value) > 0 && value === port;
+ // The companion always binds IPv4. localhost can resolve to another address family.
+ if (matchesPort(owner.loopbackPort) && url.hostname === "127.0.0.1") return true;
+ if (!matchesPort(owner.port)) return false;
+ return url.hostname === "127.0.0.1" ? ["127.0.0.1", "0.0.0.0"].includes(owner.hostname)
+ : ["::1", "[::1]", "::", "[::]"].includes(owner.hostname);
+ } catch { return false; }
+}
+export function createNativeRoutingVerifier(codexHome: string, readOwner = nativeCompatibilityOwner) {
+ const path = join(codexHome, "config.toml");
+ let baseline: string | undefined, invalidated = false;
+ return () => {
+ if (invalidated) return false;
+ try {
+ // Process-level overrides could put the app on another transport despite its TOML.
+ if (["CODEX_API_BASE_URL", "CODEX_APP_SERVER_WS_URL", "CODEX_ELECTRON_USER_DATA_PATH", "ELECTRON_RUN_AS_NODE"].some(key => process.env[key]?.trim())) { invalidated = true; return false; }
+ const stat = lstatSync(path);
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) { invalidated = true; return false; }
+ const text = readFileSync(path, "utf8"), owner = readOwner();
+ if (!owner || !matchesNativeCompatibilityRouting(text, owner)) { invalidated = true; return false; }
+ const current = routingDigest(text, owner);
+ if (baseline !== undefined && current !== baseline) { invalidated = true; return false; }
+ baseline ??= current;
+ return true;
+ } catch { invalidated = true; return false; }
+ };
+}
diff --git a/src/codex/desktop-compatibility/runtime-ownership.ts b/src/codex/desktop-compatibility/runtime-ownership.ts
new file mode 100644
index 00000000000..3bf8082eeaf
--- /dev/null
+++ b/src/codex/desktop-compatibility/runtime-ownership.ts
@@ -0,0 +1,28 @@
+import { randomUUID } from "node:crypto";
+
+interface LaunchContext { pacUrl: string; generation: string; current(): boolean }
+/** Process-local ownership; the management sibling guard owns the cross-instance boundary. */
+let owner: { token: symbol; kind: "runtime" | "certificate"; launch?: LaunchContext } | null = null;
+export function desktopCompatibilityRuntimeActive(): boolean { return owner?.kind === "runtime"; }
+function acquire(kind: "runtime" | "certificate"): () => void {
+ if (owner !== null) throw new Error("desktop_compatibility_busy");
+ const token = Symbol(); owner = { token, kind };
+ return () => { if (owner?.token === token) owner = null; };
+}
+export const acquireDesktopCompatibilityRuntime = () => acquire("runtime");
+export const acquireDesktopCertificateMutation = () => acquire("certificate");
+
+/** Published only by a successfully started runtime, never reconstructed from disk or app arguments. */
+export function bindDesktopCompatibilityLaunch(pacUrl: string, current: () => boolean): () => void {
+ if (owner?.kind !== "runtime" || owner.launch) throw new Error("desktop_compatibility_launch_owner_unverified");
+ const lease = owner, launch = { pacUrl, generation: randomUUID(), current };
+ lease.launch = launch;
+ return () => { if (lease.launch === launch) delete lease.launch; };
+}
+
+/** No key loading, socket discovery, or optional-runtime construction on the ordinary restart path. */
+export function readDesktopCompatibilityLaunch(): { pacUrl: string; generation: string } | null {
+ const launch = owner?.kind === "runtime" ? owner.launch : undefined;
+ try { return launch?.current() ? { pacUrl: launch.pacUrl, generation: launch.generation } : null; }
+ catch { return null; }
+}
diff --git a/src/codex/desktop-compatibility/runtime.ts b/src/codex/desktop-compatibility/runtime.ts
new file mode 100644
index 00000000000..439db5d4276
--- /dev/null
+++ b/src/codex/desktop-compatibility/runtime.ts
@@ -0,0 +1,173 @@
+import { randomUUID } from "node:crypto";
+import { join } from "node:path";
+import { getConfigDir } from "../../config/paths";
+import { issueServerLeaf } from "../../claude/intercept/local-ca";
+import { startConnectProxy, type ConnectProxyHandle } from "../../claude/intercept/connect-proxy";
+import { didRunOptionalShutdownHooks, registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
+import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
+import { desktopOutboundFetch, desktopProxyFor } from "../../lib/desktop-proxy-route";
+import { inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, type StoredDesktopAuthority } from "./certificate-store";
+import { inspectWindowsCertificateTrust } from "./windows-certificate-trust";
+import { createNativeIdentityReader } from "./native-identity";
+import { UsageRelayController, type UsageIdentity } from "./usage-controller";
+import { createUsageControlledFetch } from "./usage-controlled-fetch";
+import { startDesktopRelay } from "./relay-listener";
+import { launchWindowsCodexCompatibility } from "./windows-package-launch";
+import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch } from "./runtime-ownership";
+import { createDesktopConnectionStore, type DesktopConnectionStore } from "./connection-store";
+import { createNativeRoutingVerifier } from "./routing-preflight";
+import { createInstalledBuildProbe } from "./installed-build";
+
+export { DESKTOP_COMPATIBILITY_ASSESSED_VERSION, isAssessedDesktopInstalled } from "./installed-build";
+export interface DesktopRuntimeIo {
+ platform?: string;
+ loadAuthority?: () => Promise;
+ trust?: (authority: StoredDesktopAuthority) => Promise<"trusted" | "not-trusted" | "unknown">;
+ identity?: ReturnType;
+ buildSupported?: () => boolean | Promise;
+ upstreamFetch?: typeof fetch;
+ connectionStore?: DesktopConnectionStore;
+ routingSupported?: () => boolean;
+ /** Synthetic-test execution is admitted only with explicit fixture identity and CA seams. */
+ testOnly?: boolean;
+}
+
+/** Optional runtime. Import/construction/status do not start listeners or touch credentials. */
+export function createDesktopCompatibilityRuntime(io: DesktopRuntimeIo = {}) {
+ const platform = io.platform ?? process.platform;
+ let buildProbe: ReturnType | undefined;
+ const buildSupported = io.buildSupported ?? (() => buildProbe?.check() ?? Promise.resolve(false));
+ let routingSupported = io.routingSupported ?? (() => false);
+ let contextFailure: "build_unverified" | "native_routing_unverified" | null = null;
+ const contextValid = async () => {
+ try {
+ if (!routingSupported()) { contextFailure = "native_routing_unverified"; return false; }
+ const supported = await buildSupported();
+ contextFailure = !routingSupported() ? "native_routing_unverified" : !supported ? "build_unverified" : null;
+ }
+ catch { contextFailure = "build_unverified"; }
+ return contextFailure === null;
+ };
+ let phase: "off" | "starting" | "running" | "stopping" | "cleanup-required" = "off";
+ let owned: { controller: UsageRelayController; close(): Promise; pacUrl: string; fingerprint: string; deadline: number } | null = null;
+ let closing: Promise | null = null;
+ let pendingCleanup: (() => Promise) | null = null;
+ let releaseOwner: (() => void) | null = null;
+ const status = () => ({ phase, supported: platform === "win32", running: owned !== null, contextFailure,
+ ...(owned ? { fingerprint: owned.fingerprint, safetyDeadline: owned.deadline, usage: owned.controller.snapshot() } : {}) });
+ async function stop(): Promise> {
+ if (phase === "starting") throw new Error("desktop_compatibility_busy");
+ if (closing) { await closing; return status(); }
+ if (!pendingCleanup) return status();
+ phase = "stopping";
+ closing = pendingCleanup().then(() => { owned = null; pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; })
+ .catch(error => { phase = "cleanup-required"; throw error; }).finally(() => { closing = null; });
+ await closing; return status();
+ }
+ async function start() {
+ if (phase !== "off") throw new Error("desktop_compatibility_busy");
+ if (platform !== "win32") throw new Error("desktop_compatibility_unsupported");
+ if (isTestHomeGuardArmed() && !(io.testOnly && io.identity && io.loadAuthority && io.trust && io.buildSupported && io.connectionStore && io.routingSupported)) throw new Error("desktop_compatibility_test_environment");
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
+ try { desktopProxyFor(new URL("https://chatgpt.com")); } catch { throw new Error("desktop_compatibility_egress_proxy_invalid"); }
+ releaseOwner = acquireDesktopCompatibilityRuntime(); phase = "starting";
+ if (!io.buildSupported) buildProbe = createInstalledBuildProbe();
+ let relay: Awaited> | undefined, proxy: ConnectProxyHandle | undefined;
+ let pac: ReturnType | undefined, timer: ReturnType | undefined;
+ let detach: (() => void) | undefined, unbindLaunch: (() => void) | undefined, controller: UsageRelayController | undefined;
+ const cleanup = async () => {
+ unbindLaunch?.();
+ if (timer) clearInterval(timer); detach?.();
+ // Abort transport before awaiting stream refresh so a stuck reader cannot retain sockets.
+ const results = await Promise.allSettled([pac?.stop(true), proxy?.close(), relay?.close(), controller?.observeOnly(), buildProbe?.close()]);
+ if (results.some(result => result.status === "rejected")) throw new Error("desktop_compatibility_cleanup_incomplete");
+ };
+ pendingCleanup = cleanup;
+ try {
+ if (!io.routingSupported) routingSupported = createNativeRoutingVerifier((await import("../paths")).getCodexHome());
+ if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
+ const directory = join(getConfigDir(), "codex-desktop-compatibility");
+ if (!io.loadAuthority) {
+ const saved = inspectDesktopCompatibilityAuthority(directory);
+ if (saved.status === "missing") throw new Error("desktop_compatibility_certificate_not_prepared");
+ if (saved.status === "invalid") throw new Error("desktop_compatibility_certificate_invalid");
+ if (saved.status === "expired") throw new Error("desktop_compatibility_certificate_expired");
+ }
+ const authority = await (io.loadAuthority?.() ?? loadDesktopCompatibilityAuthority({ directory }));
+ const trust = await (io.trust?.(authority) ?? inspectWindowsCertificateTrust(authority.authority.certPem, authority.fingerprint));
+ if (trust !== "trusted") throw new Error("desktop_compatibility_trust_required");
+ const connections = io.connectionStore ?? createDesktopConnectionStore(directory);
+ const previousConnection = connections.read();
+ const deadline = authority.expiresAt - 300_000;
+ if (deadline <= Date.now()) throw new Error("desktop_compatibility_certificate_expiring");
+ const identity = io.identity ?? createNativeIdentityReader(join((await import("../paths")).getCodexHome(), "auth.json"), desktopOutboundFetch as typeof fetch);
+ const account: UsageIdentity | null = await identity.verifyFreshIdentity();
+ if (!account) throw new Error("desktop_compatibility_native_identity_unverified");
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
+ controller = new UsageRelayController(account, identity.readCurrentIdentity, identity.verifyFreshIdentity, Date.now, deadline, 180000, contextValid);
+ relay = await startDesktopRelay({ leaf: issueServerLeaf(authority.authority, authority.commonName, ["chatgpt.com"]),
+ fetchImpl: createUsageControlledFetch(controller, io.upstreamFetch ?? desktopOutboundFetch as typeof fetch) });
+ try { proxy = await startConnectProxy(previousConnection?.connectPort ?? 0, { interceptPort: relay.port, interceptHosts: ["chatgpt.com"], allowedTargets: ["chatgpt.com:443"] }); }
+ catch { throw new Error("desktop_compatibility_connection_unavailable"); }
+ const runId = previousConnection?.id ?? randomUUID(), proxyPort = proxy.port;
+ try { pac = Bun.serve({ hostname: "127.0.0.1", port: previousConnection?.pacPort ?? 0, fetch(req) {
+ const url = new URL(req.url);
+ if (req.method !== "GET" || req.headers.has("origin") || url.origin !== `http://127.0.0.1:${pac!.port}` || url.pathname !== `/${runId}/proxy.pac`) return new Response(null, { status: 404 });
+ return new Response(`function FindProxyForURL(url, host) { return Date.now() < ${deadline} && host.toLowerCase() === "chatgpt.com" && url.indexOf("https:") === 0 ? "PROXY 127.0.0.1:${proxyPort}; DIRECT" : "DIRECT"; }`,
+ { headers: { "content-type": "application/x-ns-proxy-autoconfig", "cache-control": "no-store" } });
+ } }); } catch { throw new Error("desktop_compatibility_connection_unavailable"); }
+ const connection = await connections.publish({ version: 1, id: runId, connectPort: proxyPort, pacPort: pac.port! });
+ if (connection.id !== runId || connection.connectPort !== proxyPort || connection.pacPort !== pac.port) throw new Error("desktop_compatibility_connection_changed");
+ if (didRunOptionalShutdownHooks()) throw new Error("desktop_compatibility_stopping");
+ const active = controller; let ticking = false, deadlineHandled = false, lastContextCheck = Date.now();
+ timer = setInterval(() => {
+ if (ticking) return; ticking = true;
+ const expired = Date.now() >= deadline;
+ const work = (async () => {
+ const contextCadence = active.snapshot().mode === "apply" ? 10000 : 60000;
+ if (!expired && Date.now() - lastContextCheck >= contextCadence) {
+ lastContextCheck = Date.now();
+ if (!await contextValid() && active.snapshot().mode === "apply") { await active.observeOnly(); return; }
+ }
+ await (expired && !deadlineHandled ? (deadlineHandled = true, active.observeOnly()) : active.expireIfNeeded());
+ })();
+ void work.catch(() => {}).finally(() => { ticking = false; });
+ }, 1000); timer.unref();
+ owned = { controller, close: cleanup, fingerprint: authority.fingerprint, deadline, pacUrl: `http://127.0.0.1:${pac.port}/${runId}/proxy.pac` };
+ phase = "running";
+ const launchedBy = owned;
+ unbindLaunch = bindDesktopCompatibilityLaunch(owned.pacUrl,
+ () => owned === launchedBy && phase === "running" && Date.now() < deadline);
+ detach = registerOptionalShutdownHook("codex-desktop-compatibility", () => { void stop().catch(() => {}); });
+ return status();
+ } catch (error) {
+ try { await cleanup(); pendingCleanup = null; phase = "off"; releaseOwner?.(); releaseOwner = null; }
+ catch { phase = "cleanup-required"; throw new Error("desktop_compatibility_cleanup_incomplete"); }
+ throw error;
+ }
+ }
+ async function apply(accountWideConsent: boolean) {
+ const current = owned;
+ if (!current || phase !== "running") throw new Error("desktop_compatibility_not_running");
+ const generation = current.controller.snapshot().generation, valid = await contextValid();
+ if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
+ if (generation !== current.controller.snapshot().generation) return { accepted: false, reason: "superseded", ...current.controller.snapshot() };
+ if (!valid) { await current.controller.observeOnly(); throw new Error(`desktop_compatibility_${contextFailure}`); }
+ const result = await current.controller.activate({ scope: "account-ui-compatibility", accountWideConsent });
+ if (owned !== current || phase !== "running") { await current.controller.observeOnly(); return { ...result, accepted: false, reason: "runtime-stopped" }; }
+ return result;
+ }
+ return { status, start, stop, apply,
+ async observe() { if (owned) await owned.controller.observeOnly(); return status(); },
+ async launch() {
+ if (!owned || phase !== "running") throw new Error("desktop_compatibility_not_running");
+ const current = owned;
+ if (!await contextValid()) throw new Error(`desktop_compatibility_${contextFailure}`);
+ if (owned !== current || phase !== "running") throw new Error("desktop_compatibility_not_running");
+ return launchWindowsCodexCompatibility(current.pacUrl);
+ },
+ /** Internal native launch context; management status never returns the control endpoint. */
+ getPacUrl: () => owned?.pacUrl ?? null,
+ };
+}
+export type DesktopCompatibilityRuntime = ReturnType;
diff --git a/src/codex/desktop-compatibility/service.ts b/src/codex/desktop-compatibility/service.ts
new file mode 100644
index 00000000000..64ada008bff
--- /dev/null
+++ b/src/codex/desktop-compatibility/service.ts
@@ -0,0 +1,30 @@
+import { createDesktopCompatibilityRuntime, type DesktopCompatibilityRuntime } from "./runtime";
+
+/** One runtime for management and automatic startup; shutdown never constructs a new instance. */
+export function createDesktopCompatibilityService(factory: () => DesktopCompatibilityRuntime = createDesktopCompatibilityRuntime) {
+ let runtime: DesktopCompatibilityRuntime | undefined;
+ let starting: Promise> | null = null;
+ let shuttingDown = false;
+ return {
+ get(): DesktopCompatibilityRuntime {
+ if (!runtime) {
+ const raw = factory();
+ runtime = { ...raw, start() {
+ if (shuttingDown) return Promise.reject(new Error("desktop_compatibility_stopping"));
+ if (starting) return Promise.reject(new Error("desktop_compatibility_busy"));
+ starting = raw.start().finally(() => { starting = null; });
+ return starting;
+ } };
+ }
+ return runtime;
+ },
+ async shutdown(): Promise {
+ shuttingDown = true;
+ try { await starting; } catch { /* failed start owns its cleanup */ }
+ if (runtime) await runtime.stop();
+ },
+ };
+}
+const service = createDesktopCompatibilityService();
+export const getDesktopCompatibilityRuntime = () => service.get();
+export const shutdownDesktopCompatibility = () => service.shutdown();
diff --git a/src/codex/desktop-compatibility/startup-settings.ts b/src/codex/desktop-compatibility/startup-settings.ts
new file mode 100644
index 00000000000..4ffba406b89
--- /dev/null
+++ b/src/codex/desktop-compatibility/startup-settings.ts
@@ -0,0 +1,50 @@
+import { createHash } from "node:crypto";
+import { readConfigFileSnapshot } from "../../config/diagnostics";
+import { mutatePersistedConfig } from "../../config/persisted-mutation";
+import { desktopCompatibilityConfigError } from "../../config/schema/desktop-compatibility";
+import { adoptPersistedDesktopCompatibility } from "../../config/live-reconcile";
+import type { OcxConfig } from "../../types";
+
+export interface DesktopStartupSettings { startOnProxyStart: boolean; revision: string }
+interface SettingsIo { read?: typeof readConfigFileSnapshot; mutate?: typeof mutatePersistedConfig; liveConfig?: OcxConfig }
+const failure = (code: string) => new Error(`desktop_compatibility_settings_${code}`);
+/** A next-process preference: never changes live runtime, trust, app or login state. */
+export function createDesktopStartupSettings(io: SettingsIo = {}) {
+ const read = io.read ?? readConfigFileSnapshot, mutate = io.mutate ?? mutatePersistedConfig;
+ function inspect() {
+ const snapshot = read();
+ if (snapshot.diagnostics.source !== "file" || snapshot.raw === undefined) throw failure("unavailable");
+ let raw: Record;
+ try { raw = JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")); } catch { throw failure("unavailable"); }
+ if (desktopCompatibilityConfigError(raw)) throw failure("invalid");
+ const setting = raw.desktopCompatibility as { startOnProxyStart: boolean } | undefined;
+ return { setting, status: { startOnProxyStart: setting?.startOnProxyStart === true,
+ revision: createHash("sha256").update(JSON.stringify(setting ?? null)).digest("hex") } };
+ }
+ const status = (): DesktopStartupSettings => inspect().status;
+ function set(startOnProxyStart: boolean, expectedRevision: string): DesktopStartupSettings {
+ if (typeof startOnProxyStart !== "boolean" || !/^[a-f0-9]{64}$/.test(expectedRevision)) throw failure("invalid_request");
+ let outcome;
+ try { outcome = mutate(config => {
+ // Runs under the existing config lock, including each rebase retry. Unrelated fields
+ // may change, but a changed preference must be shown again before this write.
+ const current = status();
+ if (current.revision !== expectedRevision) throw failure("changed");
+ if (current.startOnProxyStart === startOnProxyStart) return { changed: false, value: startOnProxyStart };
+ config.desktopCompatibility = { startOnProxyStart };
+ return { changed: true, value: startOnProxyStart };
+ }); } catch (error) {
+ // Publication can succeed before bookkeeping fails. Reconcile a verified disk
+ // field without claiming success or replaying the uncertain write.
+ if (io.liveConfig) { try { adoptPersistedDesktopCompatibility(io.liveConfig, inspect().setting); } catch { /* unresolved state remains an error */ } }
+ throw error;
+ }
+ if (outcome.status === "unavailable") throw failure("unavailable");
+ const actual = inspect();
+ if (io.liveConfig) adoptPersistedDesktopCompatibility(io.liveConfig, actual.setting);
+ if (actual.status.startOnProxyStart !== startOnProxyStart) throw failure("changed");
+ return actual.status;
+ }
+ return { status, set };
+}
+export type DesktopStartupSettingsService = ReturnType;
diff --git a/src/codex/desktop-compatibility/usage-activation.ts b/src/codex/desktop-compatibility/usage-activation.ts
new file mode 100644
index 00000000000..cdd92e363fb
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-activation.ts
@@ -0,0 +1,86 @@
+/** Account-bound, time-limited compatibility activation; no persistent settings or timers. */
+export type Observation = 'exhausted' | 'available' | 'protected';
+export type ApplyScope = 'selected-external-model' | 'account-ui-compatibility';
+type RefreshResult = {requested:number;closed:number;failed:number};
+export class UsageActivation {
+ private mode: 'observe' | 'apply' = 'observe';
+ private phase = 'observing';
+ private generation = 0;
+ private latest: { kind:Observation; at:number } | null = null;
+ private startedAt: number | null = null;
+ private outputs = 0;
+ constructor(private binding:string, private refresh:(binding:string)=>Promise,
+ private verifyIdentity:()=>Promise,
+ private clock:()=>number=Date.now, private timeoutMs=180000) {
+ if(!binding || !Number.isSafeInteger(timeoutMs) || timeoutMs<1 || timeoutMs>180000) throw new Error('Invalid activation limits');
+ }
+ snapshot() { return {mode:this.mode,phase:this.phase,generation:this.generation,outputs:this.outputs,
+ appCacheConfirmed:false,requestedUiScope:'account-wide',providerIsolationAvailable:false}; }
+ invalidateIdentity() {
+ this.mode='observe';this.phase='identity-invalidated';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
+ }
+ observe(binding:string,kind:Observation) {
+ if(binding!==this.binding)return false;
+ this.latest={kind,at:this.clock()};
+ // A recovered or protected account must not silently re-enter a previous trial
+ // if a later snapshot becomes exhausted again. A new trial requires consent.
+ if(this.mode==='apply'&&kind!=='exhausted'){
+ this.mode='observe';this.phase=kind==='available'?'usage-recovered':'protected-observation';
+ this.startedAt=null;this.outputs=0;++this.generation;
+ }
+ return true;
+ }
+ private async reset(phase:string) {
+ this.mode='observe';this.phase=phase;this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
+ return this.refresh(this.binding);
+ }
+ async activate(options:{scope:ApplyScope;accountWideConsent:boolean}) {
+ if(options.scope!=='account-ui-compatibility')return{accepted:false,reason:'selected-model-scope-unavailable',...this.snapshot()};
+ if(!options.accountWideConsent)return{accepted:false,reason:'account-wide-scope-not-accepted',...this.snapshot()};
+ const beforeVerification=this.generation,binding=this.binding;
+ let verified:string|null;
+ try{verified=await this.verifyIdentity();}catch{verified=null;}
+ if(verified!==binding||this.binding!==binding||this.generation!==beforeVerification)return{accepted:false,reason:'identity-not-verified',...this.snapshot()};
+ if(this.mode==='apply')return{accepted:false,reason:'activation-already-pending',...this.snapshot()};
+ const age=this.latest?this.clock()-this.latest.at:Infinity;
+ if(!this.latest||this.latest.kind!=='exhausted'||age<0||age>300000)return{accepted:false,reason:'no-fresh-eligible-exhaustion',...this.snapshot()};
+ // An exhausted observation authorizes one trial, not every retry within its TTL.
+ this.latest=null;
+ this.mode='apply';this.phase='awaiting-fresh-usage';this.startedAt=this.clock();this.outputs=0;
+ const generation=++this.generation;
+ let refreshed:RefreshResult;
+ try { refreshed=await this.refresh(this.binding); }
+ catch {
+ if(this.generation===generation){this.mode='observe';this.phase='refresh-failed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;}
+ return{accepted:false,reason:'refresh-failed',...this.snapshot()};
+ }
+ // Account/mode changes while a close callback awaited invalidate this activation.
+ if(this.generation!==generation)return{accepted:false,reason:'superseded',...this.snapshot()};
+ // The registry settles every close and reports failures; it does not reject the batch.
+ // Partial closure cannot leave rewriting enabled while an old app cache may survive.
+ if(!Number.isSafeInteger(refreshed.requested)||!Number.isSafeInteger(refreshed.closed)
+ ||!Number.isSafeInteger(refreshed.failed)||refreshed.requested<0||refreshed.closed<0
+ ||refreshed.failed!==0||refreshed.closed!==refreshed.requested){
+ this.mode='observe';this.phase='refresh-failed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
+ return{accepted:false,reason:'refresh-failed',refresh:refreshed,...this.snapshot()};
+ }
+ return{accepted:true,reason:'awaiting-new-response',refresh:refreshed,...this.snapshot()};
+ }
+ recordOutput(binding:string,generation:number) {
+ if(binding!==this.binding||generation!==this.generation||this.mode!=='apply')return false;
+ if(this.startedAt===null||this.clock()-this.startedAt<0||this.clock()-this.startedAt>=this.timeoutMs)return false;
+ ++this.outputs;this.phase='response-produced-app-confirmation-needed';return true;
+ }
+ async expireIfNeeded() {
+ if(this.startedAt===null||this.clock()-this.startedAt=0)return false;
+ await this.reset('expired-awaiting-original-response');return true;
+ }
+ async observeOnly() { return this.reset('observing-awaiting-original-response'); }
+ async changeBinding(binding:string) {
+ if(!binding)throw new Error('Verified account binding is required');
+ const previous=this.binding;
+ this.mode='observe';this.phase='account-changed';this.startedAt=null;this.latest=null;this.outputs=0;++this.generation;
+ this.binding=binding;
+ return this.refresh(previous);
+ }
+}
diff --git a/src/codex/desktop-compatibility/usage-controlled-fetch.ts b/src/codex/desktop-compatibility/usage-controlled-fetch.ts
new file mode 100644
index 00000000000..f0f0e6d1fb8
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-controlled-fetch.ts
@@ -0,0 +1,59 @@
+import { UsageRelayController } from './usage-controller';
+import { controlledUsageSse } from './usage-sse-controller';
+import { boundedJsonBody } from './json-body';
+
+/** Inject as the listener's fetchImpl. Existing auth forwarding/TLS/WS code stays unchanged. */
+export function createUsageControlledFetch(controller: UsageRelayController, upstreamFetch: typeof fetch = fetch): typeof fetch {
+ return (async (input: Parameters[0], init?: Parameters[1]) => {
+ const upstream = await upstreamFetch(input, init);
+ const pathname = new URL(input instanceof Request ? input.url : String(input)).pathname;
+ const method = init?.method ?? (input instanceof Request ? input.method : 'GET');
+ const exchange = { pathname, method, status: upstream.status };
+ if (method !== 'GET' || upstream.status !== 200 || !upstream.body
+ || !['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(pathname)) return upstream;
+ const contentType = upstream.headers.get('content-type') ?? '';
+ const headers = new Headers(upstream.headers);
+ const output = (body: BodyInit, transformed = false) => {
+ headers.delete('content-length'); headers.delete('content-encoding');
+ if (transformed) {
+ for (const name of ['etag', 'last-modified', 'digest', 'content-md5']) headers.delete(name);
+ headers.set('cache-control', 'no-store');
+ }
+ return new Response(body, { status: upstream.status, statusText: upstream.statusText, headers });
+ };
+ if (contentType.includes('application/json') || contentType.endsWith('+json')) {
+ const body = await boundedJsonBody(upstream.body);
+ if ('stream' in body) return output(body.stream);
+ let text: string;
+ try { text = new TextDecoder('utf-8', { fatal: true }).decode(body.bytes); }
+ catch { return output(new Uint8Array(body.bytes)); }
+ const changed = await controller.rewriteJson(text, exchange);
+ return output(changed ?? new Uint8Array(body.bytes), changed !== null);
+ }
+ if (!contentType.includes('text/event-stream') || pathname !== '/backend-api/wham/usage/stream') return upstream;
+ let registration: ReturnType = null;
+ const reader = upstream.body.pipeThrough(controlledUsageSse(text => controller.rewriteJson(text, exchange, registration))).getReader();
+ let ended = false, sink: ReadableStreamDefaultController;
+ const close = async () => {
+ if (ended) return;
+ ended = true; registration?.release();
+ const cancelled = reader.cancel();
+ try { sink.close(); } catch { /* caller already cancelled */ }
+ await cancelled;
+ };
+ const body = new ReadableStream({
+ start(value) { sink = value; },
+ async pull(value) {
+ try {
+ const next = await reader.read();
+ if (ended) return;
+ if (next.done) { ended = true; registration?.release(); value.close(); }
+ else value.enqueue(next.value);
+ } catch (error) { if (!ended) { ended = true; registration?.release(); value.error(error); } }
+ },
+ cancel: close,
+ });
+ registration = controller.registerStream(exchange, close);
+ return output(body, true);
+ }) as typeof fetch;
+}
diff --git a/src/codex/desktop-compatibility/usage-controller.ts b/src/codex/desktop-compatibility/usage-controller.ts
new file mode 100644
index 00000000000..9e9c6403fb4
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-controller.ts
@@ -0,0 +1,107 @@
+import { UsageActivation, type ApplyScope } from './usage-activation';
+import { UsageRefreshRegistry } from './usage-refresh';
+import { evaluateUsageRewrite, type UsageRewriteContext } from './usage-policy';
+
+export type UsageIdentity = NonNullable & { credentialGeneration?: string };
+type Registration = NonNullable>;
+type Exchange = { method: string; pathname: string; status: number };
+const same = (a: UsageIdentity | null, b: UsageIdentity) => a !== null
+ && a.id === b.id && a.userId === b.userId && a.plan === b.plan && a.structure === b.structure
+ && a.credentialGeneration === b.credentialGeneration;
+const object = (value: unknown): value is Record => value !== null
+ && typeof value === 'object' && !Array.isArray(value);
+
+/** Response-level controller. Construction never starts a listener, timer or trust operation. */
+export class UsageRelayController {
+ private readonly account: UsageIdentity;
+ private readonly key: string;
+ private readonly refresh = new UsageRefreshRegistry();
+ private readonly activation: UsageActivation;
+ private jsonSnapshots = 0;
+ private streamSnapshots = 0;
+ private lastSnapshotAt: number | null = null;
+ constructor(account: UsageIdentity, private readonly readCurrentIdentity: () => Promise,
+ verifyFreshIdentity: () => Promise, private readonly clock: () => number,
+ private readonly expiresAt: number, timeoutMs = 180000, private readonly contextValid: () => boolean | Promise = () => true) {
+ if (!account.id || !account.userId || !['plus', 'pro'].includes(account.plan)
+ || account.structure !== 'personal' || !Number.isFinite(expiresAt) || expiresAt <= clock()) {
+ throw new Error('A verified supported identity and finite safety deadline are required');
+ }
+ this.account = { ...account };
+ this.key = JSON.stringify([account.id, account.userId, account.plan, account.credentialGeneration ?? null]);
+ this.activation = new UsageActivation(this.key, key => this.refresh.refresh(key), async () => {
+ return same(await verifyFreshIdentity(), this.account) ? this.key : null;
+ }, clock, timeoutMs);
+ }
+ snapshot() { return { ...this.activation.snapshot(), trackedStreams: this.refresh.size, expired: this.clock() >= this.expiresAt,
+ observation: { jsonSnapshots: this.jsonSnapshots, streamSnapshots: this.streamSnapshots,
+ validatedActiveStreams: this.refresh.boundSize, lastSnapshotAt: this.lastSnapshotAt,
+ sourceProcessVerified: false as const, composerRecoveryVerified: false as const } }; }
+ private async checkContext() { try { return await this.contextValid(); } catch { return false; } }
+ async activate(options: { scope: ApplyScope; accountWideConsent: boolean }) {
+ const generation = this.activation.snapshot().generation, valid = await this.checkContext();
+ if (generation !== this.activation.snapshot().generation) return { accepted: false, reason: 'superseded', ...this.snapshot() };
+ if (!valid) { this.activation.invalidateIdentity(); return { accepted: false, reason: 'native-context-changed', ...this.snapshot() }; }
+ if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); return { accepted: false, reason: 'safety-deadline', ...this.snapshot() }; }
+ return this.activation.activate(options);
+ }
+ observeOnly() { return this.activation.observeOnly(); }
+ expireIfNeeded() { return this.activation.expireIfNeeded(); }
+ registerStream(exchange: Exchange, close: () => Promise): Registration | null {
+ if (exchange.status !== 200) return null;
+ return this.refresh.register(exchange.method, exchange.pathname, close);
+ }
+ /** Only a complete original usage record may establish the stream's account binding. */
+ async rewriteJson(text: string, exchange: Exchange, stream?: Registration | null): Promise {
+ if (exchange.method !== 'GET' || exchange.status !== 200
+ || !['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(exchange.pathname)) return null;
+ if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ const observedGeneration = this.activation.snapshot().generation;
+ let current: UsageIdentity | null;
+ try { current = await this.readCurrentIdentity(); } catch { current = null; }
+ if (!same(current, this.account)) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ // Do not apply an activation that raced this response's identity read.
+ if (observedGeneration !== this.activation.snapshot().generation) return null;
+ if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ if (Buffer.byteLength(text, 'utf8') > 262144) { stream?.exclude(); return null; }
+ let parsed: unknown;
+ try { parsed = JSON.parse(text); } catch { stream?.exclude(); return null; }
+ const envelope = object(parsed) && 'usage' in parsed ? parsed : null;
+ if ((exchange.pathname.endsWith('/stream') && !envelope)
+ || (envelope && (envelope.version !== 1 || typeof envelope.stream_id !== 'string' || !envelope.stream_id
+ || !Number.isSafeInteger(envelope.sequence) || Number(envelope.sequence) <= 0))) { stream?.exclude(); return null; }
+ const original = envelope ? envelope.usage : parsed;
+ if (!object(original) || original.account_id !== this.account.id || original.user_id !== this.account.userId
+ || original.plan_type !== this.account.plan) { stream?.exclude(); return null; }
+ const rate = original.rate_limit;
+ if (!object(rate) || typeof rate.allowed !== 'boolean' || typeof rate.limit_reached !== 'boolean') {
+ stream?.exclude(); return null;
+ }
+ if (stream && !stream.bind(this.key)) return null;
+ // Identity-checked responses received by this relay do not establish the
+ // sending process or prove that an authoritative UI cache consumed them.
+ if (exchange.pathname.endsWith('/stream')) this.streamSnapshots = Math.min(Number.MAX_SAFE_INTEGER, this.streamSnapshots + 1);
+ else this.jsonSnapshots = Math.min(Number.MAX_SAFE_INTEGER, this.jsonSnapshots + 1);
+ this.lastSnapshotAt = this.clock();
+ const context: UsageRewriteContext = { enabled: true, mode: 'observe', status: exchange.status,
+ pathname: exchange.pathname, account: this.account };
+ const observed = evaluateUsageRewrite(original, context);
+ this.activation.observe(this.key, observed.eligible ? 'exhausted'
+ : rate.allowed === true && rate.limit_reached === false ? 'available' : 'protected');
+ const state = this.activation.snapshot();
+ if (state.mode !== 'apply') return null;
+ const result = evaluateUsageRewrite(original, { ...context, mode: 'apply' });
+ if (!result.changed) return null;
+ // Observe and unchanged responses never spawn an installed-package probe.
+ const valid = await this.checkContext();
+ if (state.generation !== this.activation.snapshot().generation) return null;
+ if (!valid) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ // A native account or trial can change while the asynchronous build check runs.
+ try { current = await this.readCurrentIdentity(); } catch { current = null; }
+ if (state.generation !== this.activation.snapshot().generation) return null;
+ if (!same(current, this.account)) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ if (this.clock() >= this.expiresAt) { this.activation.invalidateIdentity(); stream?.exclude(); return null; }
+ if (!this.activation.recordOutput(this.key, state.generation)) return null;
+ return JSON.stringify(envelope ? { ...envelope, usage: result.value } : result.value);
+ }
+}
diff --git a/src/codex/desktop-compatibility/usage-policy.ts b/src/codex/desktop-compatibility/usage-policy.ts
new file mode 100644
index 00000000000..7a87d10f0ff
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-policy.ts
@@ -0,0 +1,49 @@
+/** Pure response policy. Real quota windows, credits and spending restrictions stay unchanged. */
+export interface UsageRewriteContext {
+ enabled: boolean;
+ mode: 'observe' | 'apply';
+ status: number;
+ pathname: string;
+ account: {
+ id: string;
+ userId: string;
+ plan: 'plus' | 'pro';
+ structure: 'personal';
+ } | null;
+}
+
+type RecordValue = Record;
+function record(value: unknown): value is RecordValue {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+}
+
+/**
+ * Transform only a complete, matched personal-account usage snapshot.
+ * Account context must come from a trusted live integration; this module does not
+ * discover it or prove provider isolation. Never persist the adjusted snapshot as
+ * actual provider/account capacity. Unknown or protected states pass unchanged.
+ */
+export function evaluateUsageRewrite(value: unknown, context: UsageRewriteContext) {
+ const unchanged = (reason: string) => ({ value, changed: false, eligible: false, reason });
+ if (!context.enabled) return unchanged('off');
+ if (context.status !== 200) return unchanged('non-success-response');
+ if (!['/backend-api/wham/usage', '/backend-api/wham/usage/stream'].includes(context.pathname)) return unchanged('unowned-path');
+ const account = context.account;
+ if (!account || !account.id || !account.userId || account.structure !== 'personal' || !['plus', 'pro'].includes(account.plan)) return unchanged('unsupported-account');
+ if (!record(value) || value.account_id !== account.id || value.user_id !== account.userId || value.plan_type !== account.plan) return unchanged('identity-mismatch');
+ const rate = value.rate_limit;
+ if (!record(rate) || rate.allowed !== false || rate.limit_reached !== true) return unchanged('not-explicitly-exhausted');
+ const spend = value.spend_control;
+ // Absence is unknown, not evidence that no protected spending restriction exists.
+ if (!record(spend) || spend.reached !== false) return unchanged('protected-or-unknown-spend');
+ const reason = value.rate_limit_reached_type;
+ if (reason != null && (!record(reason) || reason.type !== 'rate_limit_reached')) return unchanged('protected-or-unknown-reason');
+ const credits = value.credits;
+ if (!record(credits) || typeof credits.has_credits !== 'boolean' || typeof credits.unlimited !== 'boolean') return unchanged('unknown-credit-schema');
+ if (credits.overage_limit_reached != null && credits.overage_limit_reached !== false) return unchanged('overage-restriction');
+ if (context.mode !== 'apply') return { value, changed: false, eligible: true, reason: 'observe-only' };
+ return {
+ value: { ...value, rate_limit: { ...rate, allowed: true, limit_reached: false } },
+ changed: true, eligible: true, reason: 'personal-usage-only',
+ };
+}
diff --git a/src/codex/desktop-compatibility/usage-refresh.ts b/src/codex/desktop-compatibility/usage-refresh.ts
new file mode 100644
index 00000000000..2eea2aa1e1c
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-refresh.ts
@@ -0,0 +1,33 @@
+/** Exact usage-stream refresh registry. No timers, sockets or core-path imports. */
+export class UsageRefreshRegistry {
+ private entries = new Set<{ binding: string | null; tainted: boolean; close: () => Promise }>();
+ register(method: string, pathname: string, close: () => Promise) {
+ if (method !== 'GET' || pathname !== '/backend-api/wham/usage/stream') return null;
+ const entry = { binding: null as string | null, tainted: false, close };
+ this.entries.add(entry);
+ return {
+ // Caller must invoke only after validating a complete upstream snapshot's identity.
+ // A connection that changes account identity never becomes a refresh target again.
+ bind: (verifiedBinding: string) => {
+ if (!verifiedBinding || entry.tainted || !this.entries.has(entry)) return false;
+ if (entry.binding !== null && entry.binding !== verifiedBinding) {
+ entry.tainted = true; entry.binding = null; return false;
+ }
+ entry.binding = verifiedBinding; return true;
+ },
+ exclude: () => { entry.tainted = true; entry.binding = null; },
+ release: () => { this.entries.delete(entry); },
+ };
+ }
+ async refresh(binding: string) {
+ if (!binding) throw new Error('Verified account binding is required');
+ const selected = [...this.entries].filter(e => !e.tainted && e.binding === binding);
+ // Detach before awaiting; concurrent refreshes cannot close the same stream twice.
+ for (const entry of selected) this.entries.delete(entry);
+ const results = await Promise.allSettled(selected.map(entry => Promise.resolve().then(entry.close)));
+ return { requested: selected.length, closed: results.filter(r => r.status === 'fulfilled').length,
+ failed: results.filter(r => r.status === 'rejected').length };
+ }
+ get size() { return this.entries.size; }
+ get boundSize() { return [...this.entries].filter(entry => !entry.tainted && entry.binding !== null).length; }
+}
diff --git a/src/codex/desktop-compatibility/usage-sse-controller.ts b/src/codex/desktop-compatibility/usage-sse-controller.ts
new file mode 100644
index 00000000000..d99d458e3e6
--- /dev/null
+++ b/src/codex/desktop-compatibility/usage-sse-controller.ts
@@ -0,0 +1,86 @@
+type Rewrite = (text: string) => Promise;
+
+async function rewriteRecord(bytes: Buffer, first: boolean, rewrite: Rewrite): Promise {
+ let text: string;
+ try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
+ catch { return Buffer.from(bytes); }
+ const lines = [...text.matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)].filter(m => m[0].length > 0);
+ const data: string[] = [], indexes = new Set(); let event = '';
+ lines.forEach((line, i) => {
+ const content = first && i === 0 ? line[1]!.replace(/^\uFEFF/, '') : line[1]!;
+ if (content.startsWith(':')) return;
+ const colon = content.indexOf(':');
+ const key = colon < 0 ? content : content.slice(0, colon);
+ const value = colon < 0 ? '' : content.slice(colon + 1).replace(/^ /, '');
+ if (key === 'event') event = value;
+ if (key === 'data') { data.push(value); indexes.add(i); }
+ });
+ if (event !== 'usage.snapshot' || !data.length) return Buffer.from(bytes);
+ const changed = await rewrite(data.join('\n'));
+ if (changed === null) return Buffer.from(bytes);
+ let written = false;
+ return Buffer.from(lines.map((line, i) => {
+ if (!indexes.has(i)) return line[0];
+ if (written) return '';
+ written = true;
+ const bom = first && i === 0 && line[1]!.startsWith('\uFEFF') ? '\uFEFF' : '';
+ return `${bom}data: ${changed}${line[2]}`;
+ }).join(''));
+}
+
+/** Byte-bounded SSE framing; never fabricates events or changes stream sequence numbers. */
+export function controlledUsageSse(rewrite: Rewrite, cap = 262144): TransformStream {
+ if (!Number.isSafeInteger(cap) || cap < 1 || cap > 1048576) throw new Error('Invalid record limit');
+ const buffer = Buffer.alloc(cap);
+ let used = 0, lineHasBytes = false, pendingCR = false, first = true, passthrough = false;
+ return new TransformStream({
+ async transform(chunk, controller) {
+ let rawStart = passthrough ? 0 : -1;
+ const enterPassthrough = (start: number) => {
+ controller.enqueue(Buffer.from(buffer.subarray(0, used)));
+ used = 0; passthrough = true; rawStart = start;
+ };
+ const endRecord = async (rawEnd: number) => {
+ if (passthrough) {
+ if (rawEnd > rawStart) controller.enqueue(chunk.subarray(rawStart, rawEnd));
+ passthrough = false; rawStart = -1;
+ } else {
+ controller.enqueue(await rewriteRecord(buffer.subarray(0, used), first, rewrite));
+ }
+ first = false; used = 0; lineHasBytes = false;
+ };
+ for (let i = 0; i < chunk.length; i++) {
+ const byte = chunk[i]!;
+ if (pendingCR) {
+ pendingCR = false;
+ if (byte !== 10) {
+ if (!lineHasBytes) await endRecord(i);
+ lineHasBytes = false;
+ }
+ else {
+ if (!passthrough && used === cap) enterPassthrough(i);
+ if (!passthrough) buffer[used++] = byte;
+ const blankLine = !lineHasBytes;
+ lineHasBytes = false;
+ if (blankLine) await endRecord(i + 1);
+ continue;
+ }
+ }
+ if (!passthrough && used === cap) enterPassthrough(i);
+ if (!passthrough) buffer[used++] = byte;
+ if (byte === 13) pendingCR = true;
+ else if (byte === 10) {
+ const blankLine = !lineHasBytes;
+ lineHasBytes = false;
+ if (blankLine) await endRecord(i + 1);
+ } else lineHasBytes = true;
+ }
+ if (passthrough && rawStart < chunk.length) controller.enqueue(chunk.subarray(rawStart));
+ },
+ async flush(controller) {
+ if (passthrough) return;
+ if (pendingCR && !lineHasBytes) { controller.enqueue(await rewriteRecord(buffer.subarray(0, used), first, rewrite)); used = 0; }
+ if (used > 0) controller.enqueue(Buffer.from(buffer.subarray(0, used)));
+ },
+ });
+}
diff --git a/src/codex/desktop-compatibility/windows-activation-source.ts b/src/codex/desktop-compatibility/windows-activation-source.ts
new file mode 100644
index 00000000000..6d4c1497273
--- /dev/null
+++ b/src/codex/desktop-compatibility/windows-activation-source.ts
@@ -0,0 +1,44 @@
+// Kept inline so source and standalone Bun distributions carry the same COM adapter.
+export const WINDOWS_ACTIVATION_SOURCE = String.raw`using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+// Windows.Launch preserves package identity and forwards explicit application arguments.
+public static class OpenCodexPackageActivation {
+ [ComImport, Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IApplicationActivationManager {
+ [PreserveSig] int ActivateApplication([MarshalAs(UnmanagedType.LPWStr)] string appId, [MarshalAs(UnmanagedType.LPWStr)] string arguments, uint options, out uint processId);
+ [PreserveSig] int ActivateForFile([MarshalAs(UnmanagedType.LPWStr)] string appId, IntPtr items, [MarshalAs(UnmanagedType.LPWStr)] string verb, out uint processId);
+ [PreserveSig] int ActivateForProtocol([MarshalAs(UnmanagedType.LPWStr)] string appId, IntPtr items, out uint processId);
+ }
+ [DllImport("ole32.dll", PreserveSig=true)]
+ private static extern int CoCreateInstance(ref Guid clsid, IntPtr outer, uint context, ref Guid iid, [MarshalAs(UnmanagedType.Interface)] out IApplicationActivationManager manager);
+ [DllImport("kernel32.dll", SetLastError=true)] private static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
+ [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] private static extern int GetPackageFullName(IntPtr process, ref uint length, StringBuilder name);
+ private static IApplicationActivationManager Manager() {
+ var clsid=new Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C");
+ var iid=new Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D");
+ IApplicationActivationManager manager;
+ // LOCAL_SERVER preserves activation argument lifetime after the short launcher exits.
+ Marshal.ThrowExceptionForHR(CoCreateInstance(ref clsid,IntPtr.Zero,4,ref iid,out manager));
+ return manager;
+ }
+ public static void ValidateActivationService() {var manager=Manager();Marshal.FinalReleaseComObject(manager);}
+ public static uint Activate(string appId,string arguments) {
+ var manager=Manager();
+ try {uint pid;Marshal.ThrowExceptionForHR(manager.ActivateApplication(appId,arguments,0,out pid));return pid;}
+ finally {Marshal.FinalReleaseComObject(manager);}
+ }
+ public static string PackageOf(uint pid) {
+ var handle=OpenProcess(0x1000,false,pid);
+ if(handle==IntPtr.Zero)throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
+ try {uint length=0;int status=GetPackageFullName(handle,ref length,null);
+ if(status==15700)return null;
+ if(status!=122)throw new System.ComponentModel.Win32Exception(status);
+ var value=new StringBuilder((int)length);status=GetPackageFullName(handle,ref length,value);
+ if(status!=0)throw new System.ComponentModel.Win32Exception(status);
+ return value.ToString();
+ }finally{CloseHandle(handle);}
+ }
+}`;
diff --git a/src/codex/desktop-compatibility/windows-certificate-trust.ts b/src/codex/desktop-compatibility/windows-certificate-trust.ts
new file mode 100644
index 00000000000..d26e2f828c3
--- /dev/null
+++ b/src/codex/desktop-compatibility/windows-certificate-trust.ts
@@ -0,0 +1,81 @@
+import { execFile } from "node:child_process";
+import { X509Certificate } from "node:crypto";
+import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
+import type { StoredDesktopAuthority } from "./certificate-store";
+import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
+import { isServerAuthOnlyCertificate } from "../../claude/intercept/local-ca";
+
+export type DesktopCertificateTrust = "trusted" | "not-trusted" | "unknown";
+export type TrustOperation = "inspect" | "trust" | "remove";
+export type CertificateTrustRunner = (operation: TrustOperation, certificateDer: string, fingerprint: string) => Promise;
+
+const SCRIPT = [
+ "$ErrorActionPreference='Stop'",
+ "$inputData=[Console]::In.ReadToEnd()|ConvertFrom-Json",
+ "$bytes=[Convert]::FromBase64String($inputData.certificate)",
+ "$cert=[Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)",
+ "$sha=[Security.Cryptography.SHA256]::Create()",
+ "try {$fingerprint=([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-','')}finally{$sha.Dispose()}",
+ "if($fingerprint -cne $inputData.fingerprint){throw 'Certificate changed'}",
+ "$store=[Security.Cryptography.X509Certificates.X509Store]::new('Root','CurrentUser')",
+ "$mode=if($inputData.operation -eq 'inspect'){[Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly}else{[Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite}",
+ "try {",
+ " $store.Open($mode)",
+ " $foundCertificates=@($store.Certificates.Find([Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint,$cert.Thumbprint,$false))",
+ " foreach($certificateMatch in $foundCertificates){if([Convert]::ToBase64String($certificateMatch.RawData) -cne [Convert]::ToBase64String($bytes)){throw 'Certificate mismatch'}}",
+ " if($inputData.operation -eq 'trust'){if($foundCertificates.Count -eq 0){$store.Add($cert)}}",
+ " elseif($inputData.operation -eq 'remove'){foreach($certificateMatch in $foundCertificates){$store.Remove($certificateMatch)}}",
+ " elseif($inputData.operation -ne 'inspect'){throw 'Unknown operation'}",
+ " $present=@($store.Certificates.Find([Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint,$cert.Thumbprint,$false)).Count -gt 0",
+ " if($present){[Console]::Out.Write('trusted')}else{[Console]::Out.Write('not-trusted')}",
+ "}finally{$store.Close();$store.Dispose();$cert.Dispose()}",
+].join("\n");
+
+const run: CertificateTrustRunner = (operation, certificateDer, fingerprint) => {
+ if (process.platform !== "win32") return Promise.resolve("unknown");
+ if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
+ return new Promise(resolve => {
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
+ timeout: operation === "inspect" ? 10_000 : 120_000, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
+ }, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
+ ? stdout.trim() as DesktopCertificateTrust : "unknown"));
+ child.stdin?.on("error", () => { /* completion and readback decide the result */ });
+ child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
+ });
+};
+
+/** Public status cannot create a certificate, decrypt a key or alter OS trust. */
+export async function inspectWindowsCertificateTrust(certPem: string, expectedFingerprint: string, runner: CertificateTrustRunner = run): Promise {
+ const certificate = new X509Certificate(certPem);
+ const fingerprint = certificate.fingerprint256.replaceAll(":", "");
+ if (!certificate.ca || fingerprint !== expectedFingerprint || !/^[A-F0-9]{64}$/.test(expectedFingerprint)) {
+ throw new Error("desktop_compatibility_certificate_mismatch");
+ }
+ return runner("inspect", certificate.raw.toString("base64"), fingerprint).catch(() => "unknown" as const);
+}
+
+/** Mutations require the store-validated private key, not unverified public status metadata. */
+export function createWindowsCertificateTrust(authority: StoredDesktopAuthority, expectedFingerprint: string, runner: CertificateTrustRunner = run) {
+ const certificate = new X509Certificate(authority.authority.certPem);
+ const fingerprint = certificate.fingerprint256.replaceAll(":", "");
+ if (fingerprint !== expectedFingerprint || !certificate.ca || !certificate.checkPrivateKey(authority.authority.privateKey)
+ || !certificate.verify(authority.authority.publicKey) || !/^OpenCodex Codex Desktop [0-9a-f-]{36}$/.test(authority.commonName)
+ || !certificate.subject.split("\n").includes(`CN=${authority.commonName}`)) throw new Error("desktop_compatibility_certificate_mismatch");
+ const encoded = certificate.raw.toString("base64");
+ const inspect = () => runner("inspect", encoded, fingerprint).catch(() => "unknown" as const);
+ let pending: Promise | null = null;
+ const change = (operation: "trust" | "remove") => {
+ if (pending) return Promise.reject(new Error("desktop_compatibility_trust_busy"));
+ if (operation === "trust" && Date.parse(certificate.validTo) <= Date.now()) return Promise.reject(new Error("desktop_compatibility_authority_expired"));
+ if (operation === "trust" && !isServerAuthOnlyCertificate(certificate)) return Promise.reject(new Error("desktop_compatibility_authority_renewal_required"));
+ pending = (async () => {
+ const before = await inspect();
+ if (before === "unknown") return before;
+ if (before === (operation === "trust" ? "trusted" : "not-trusted")) return before;
+ try { await runner(operation, encoded, fingerprint); } catch { /* read the exact store after an uncertain action */ }
+ return inspect();
+ })().finally(() => { pending = null; });
+ return pending;
+ };
+ return { inspect, trust: () => change("trust"), remove: () => change("remove") };
+}
diff --git a/src/codex/desktop-compatibility/windows-key-protection.ts b/src/codex/desktop-compatibility/windows-key-protection.ts
new file mode 100644
index 00000000000..6c12cce52a7
--- /dev/null
+++ b/src/codex/desktop-compatibility/windows-key-protection.ts
@@ -0,0 +1,55 @@
+import { execFile } from "node:child_process";
+import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
+
+const MAX_INPUT = 65_536;
+const MAX_OUTPUT = 131_072;
+const PURPOSE = "opencodex/codex-desktop-compatibility/authority/v1";
+const SCRIPT = [
+ "$ErrorActionPreference='Stop'",
+ "Add-Type -AssemblyName System.Security",
+ "$r=[Console]::In.ReadToEnd()|ConvertFrom-Json",
+ "$data=[Convert]::FromBase64String($r.data)",
+ `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'){",
+ " $out=[Security.Cryptography.ProtectedData]::Protect($data,$entropy,$scope)",
+ "}elseif($r.operation -eq 'unprotect'){",
+ " $out=[Security.Cryptography.ProtectedData]::Unprotect($data,$entropy,$scope)",
+ "}else{throw 'Invalid operation'}",
+ "[Console]::Out.Write([Convert]::ToBase64String($out))",
+].join("\n");
+
+export interface AuthorityKeyProtection {
+ protect(cleartext: Uint8Array): Promise;
+ unprotect(ciphertext: Uint8Array): Promise;
+}
+
+/** The payload travels on stdin, never in process arguments, environment or logs. */
+function runDpapi(operation: "protect" | "unprotect", data: Uint8Array): Promise {
+ if (process.platform !== "win32") return Promise.reject(new Error("desktop_compatibility_windows_required"));
+ if (data.byteLength === 0 || data.byteLength > MAX_INPUT) return Promise.reject(new Error("desktop_compatibility_key_size"));
+ return new Promise((resolve, reject) => {
+ const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
+ timeout: 10_000, maxBuffer: MAX_OUTPUT, windowsHide: true, encoding: "utf8",
+ }, (error, stdout) => {
+ // Do not include PowerShell stderr, command output or the input in a propagated error.
+ if (error || !/^[A-Za-z0-9+/]+={0,2}$/.test(stdout.trim())) {
+ reject(new Error("desktop_compatibility_key_protection_failed")); return;
+ }
+ const bytes = Buffer.from(stdout.trim(), "base64");
+ if (bytes.length === 0 || bytes.length > MAX_INPUT) {
+ reject(new Error("desktop_compatibility_key_size")); return;
+ }
+ 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") }));
+ });
+}
+
+/** CurrentUser protects against other OS identities, not other processes of the same user. */
+export const windowsAuthorityKeyProtection: AuthorityKeyProtection = {
+ protect: bytes => runDpapi("protect", bytes),
+ unprotect: bytes => runDpapi("unprotect", bytes),
+};
diff --git a/src/codex/desktop-compatibility/windows-package-command.ts b/src/codex/desktop-compatibility/windows-package-command.ts
new file mode 100644
index 00000000000..e7909583784
--- /dev/null
+++ b/src/codex/desktop-compatibility/windows-package-command.ts
@@ -0,0 +1,86 @@
+import type { DesktopAppInstall, DesktopExec, DesktopProcess } from "../desktop-app/types";
+import { resolveTrustedWindowsPowerShellExe } from "../../lib/windows-elevation";
+import { WINDOWS_ACTIVATION_SOURCE } from "./windows-activation-source";
+import { readDesktopCompatibilityLaunch } from "./runtime-ownership";
+
+/** Only the feature's loopback PAC endpoint may become a launch argument. */
+export function validatedCompatibilityPacUrl(value: string): string {
+ let url: URL;
+ try { url = new URL(value); } catch { throw new Error("desktop_compatibility_invalid_pac_url"); }
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || Number(url.port) < 1
+ || url.username || url.password || url.search || url.hash
+ || !/^\/[a-zA-Z0-9-]{16,80}\/proxy\.pac$/.test(url.pathname)
+ || value !== url.href) throw new Error("desktop_compatibility_invalid_pac_url");
+ return url.href;
+}
+
+function literal(value: string): string { return `'${value.replaceAll("'", "''")}'`; }
+
+export function compatibilityActivationScript(install: DesktopAppInstall, pacUrl: string): string {
+ const pac = validatedCompatibilityPacUrl(pacUrl);
+ if (!/^OpenAI\.Codex(?:Beta)?_[A-Za-z0-9]+$/.test(install.id) || install.relaunch !== `${install.id}!App`) {
+ throw new Error("desktop_compatibility_invalid_package");
+ }
+ return [
+ "$ErrorActionPreference='Stop'",
+ `$family=${literal(install.id)}; $root=${literal(install.root)}; $pac=${literal(pac)}`,
+ `$p=Get-AppxPackage -Name ${literal(install.id.split("_")[0]!)}`,
+ "$p=@($p|Where-Object {$_.PackageFamilyName -eq $family -and $_.InstallLocation -ieq $root})",
+ "if($p.Count -ne 1){throw 'Package changed'}; $p=$p[0]",
+ "$manifest=Get-AppxPackageManifest -Package $p.PackageFullName",
+ "$entry=@($manifest.Package.Applications.Application|Where-Object {$_.Id -eq 'App' -and $_.Executable.Replace('/','\\') -ieq 'app\\ChatGPT.exe'})",
+ "if($entry.Count -ne 1){throw 'Application entry changed'}",
+ "Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@",
+ "$pidValue=[OpenCodexPackageActivation]::Activate($family+'!App','--proxy-pac-url='+$pac)",
+ "if([OpenCodexPackageActivation]::PackageOf($pidValue) -ne $p.PackageFullName){throw 'Package identity missing'}",
+ "$running=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue)",
+ "$expected=Join-Path $p.InstallLocation 'app\\ChatGPT.exe'",
+ "if(-not $running -or $running.ExecutablePath -ine $expected -or $running.CommandLine -notmatch ('(?:^|[\\s\"])--proxy-pac-url='+[regex]::Escape($pac)+'(?:$|[\\s\"])')){throw 'Launch arguments not observed'}",
+ "[pscustomobject]@{pid=[int]$pidValue;packageFullName=$p.PackageFullName;verified=$true}|ConvertTo-Json -Compress",
+ ].join("\n");
+}
+
+
+/** Preserve only the current runtime's PAC and lifetime, never trust a URL shape alone. */
+export function captureWindowsCompatibilityContext(processes: readonly DesktopProcess[]): Record {
+ const members = new Set(processes.map(entry => entry.pid));
+ const urls = new Set();
+ for (const entry of processes.filter(value => !members.has(value.parentPid))) {
+ if (entry.commandLine !== undefined && entry.commandLine.trim() === "") {
+ throw new Error("desktop_compatibility_launch_context_unavailable");
+ }
+ for (const match of (entry.commandLine ?? "").matchAll(/(?:^|\s)"?--proxy-pac-url=([^"\s]+)"?/g)) {
+ const url = match[1]!;
+ if (url.startsWith("http://127.0.0.1:")) urls.add(validatedCompatibilityPacUrl(url));
+ }
+ }
+ if (urls.size > 1) throw new Error("desktop_compatibility_conflicting_launch_context");
+ if (urls.size === 0) return {};
+ const pacUrl = [...urls][0]!, owner = readDesktopCompatibilityLaunch();
+ if (!owner || owner.pacUrl !== pacUrl) throw new Error("desktop_compatibility_launch_owner_unverified");
+ return { codexCompatibilityPacUrl: pacUrl, codexCompatibilityGeneration: owner.generation };
+}
+
+/** Recheck after the asynchronous stop ladder: identical endpoints can belong to a new runtime. */
+export function assertWindowsCompatibilityContext(context: Record): void {
+ const owner = readDesktopCompatibilityLaunch();
+ if (!owner || owner.pacUrl !== context.codexCompatibilityPacUrl || owner.generation !== context.codexCompatibilityGeneration) {
+ throw new Error("desktop_compatibility_launch_owner_unverified");
+ }
+}
+
+export function activateWindowsCodexCompatibility(exec: DesktopExec, install: DesktopAppInstall, pacUrl: string): { pid: number; packageFullName: string } {
+ const script = compatibilityActivationScript(install, pacUrl);
+ let text: string;
+ try { text = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 15_000, windowsHide: true }); }
+ catch { throw new Error("desktop_compatibility_activation_unverified"); }
+ let result: unknown;
+ try { result = JSON.parse(text.trim().split(/\r?\n/).at(-1) ?? ""); }
+ catch { throw new Error("desktop_compatibility_activation_unverified"); }
+ if (!result || typeof result !== "object" || Array.isArray(result)) throw new Error("desktop_compatibility_activation_unverified");
+ const value = result as Record;
+ if (value.verified !== true || !Number.isSafeInteger(value.pid) || Number(value.pid) <= 0
+ || typeof value.packageFullName !== "string" || !value.packageFullName.startsWith(install.id.split("_")[0]! + "_")
+ || !value.packageFullName.endsWith("_" + install.id.split("_")[1]!)) throw new Error("desktop_compatibility_activation_unverified");
+ return { pid: Number(value.pid), packageFullName: value.packageFullName };
+}
diff --git a/src/codex/desktop-compatibility/windows-package-launch.ts b/src/codex/desktop-compatibility/windows-package-launch.ts
new file mode 100644
index 00000000000..64074f3eeb3
--- /dev/null
+++ b/src/codex/desktop-compatibility/windows-package-launch.ts
@@ -0,0 +1,28 @@
+import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
+import type { DesktopExec } from "../desktop-app/types";
+import { activateWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "./windows-package-command";
+export { compatibilityActivationScript, validatedCompatibilityPacUrl } from "./windows-package-command";
+
+export interface CompatibilityLaunchIo {
+ exec?: DesktopExec;
+ platform?: string;
+}
+export type CompatibilityLaunchResult =
+ | { status: "started"; pid: number; packageFullName: string }
+ | { status: "refused"; reason: "unsupported_platform" | "test_environment" | "package_unavailable" | "app_running" | "process_probe_failed" | "activation_failed" };
+
+/** Never quits an app, launches the raw EXE, changes login or installs a watcher. */
+export function launchWindowsCodexCompatibility(pacUrl: string, io: CompatibilityLaunchIo = {}): CompatibilityLaunchResult {
+ validatedCompatibilityPacUrl(pacUrl);
+ if ((io.platform ?? process.platform) !== "win32") return { status: "refused", reason: "unsupported_platform" };
+ if (process.env.OCX_TEST_HOME_GUARD === "1" && !io.exec) return { status: "refused", reason: "test_environment" };
+ const exec = io.exec ?? windowsDefaultExec;
+ const install = windowsDesktopAppAdapter.discover(exec);
+ if (!install) return { status: "refused", reason: "package_unavailable" };
+ const processes = windowsDesktopAppAdapter.listProcesses(exec, install);
+ if (processes === null) return { status: "refused", reason: "process_probe_failed" };
+ if (processes.length > 0) return { status: "refused", reason: "app_running" };
+ try {
+ return { status: "started", ...activateWindowsCodexCompatibility(exec, install, pacUrl) };
+ } catch { return { status: "refused", reason: "activation_failed" }; }
+}
diff --git a/src/config/diagnostics.ts b/src/config/diagnostics.ts
index 8037724c950..31539977831 100644
--- a/src/config/diagnostics.ts
+++ b/src/config/diagnostics.ts
@@ -4,6 +4,7 @@ import { lstatSync, readFileSync } from "node:fs";
import { join } from "node:path";
import * as z from "zod/v4";
import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
+import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
import type { OcxConfig } from "../types";
import { parseAnthropicModelRoutes } from "../oauth/anthropic-model-routes";
@@ -653,7 +654,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
if (!parsed.ok) return { ok: false, error: `schema_invalid: anthropicAccountPool.routes: ${parsed.error}` };
}
const boundaryError = blockedModelRedirectsError(value)
- ?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
+ ?? compactionRecoveryConfigError(value) ?? desktopCompatibilityConfigError(value) ?? configReasoningPinsConfigError(value)
?? blankHostnameError(value)
?? (rawSubagentModelForce(value) !== undefined && !isSubagentModelEntry(rawSubagentModelForce(value)) ? "schema_invalid: claudeCode.subagentModelForce: expected a safe roster-style model id" : null)
?? claudeSubagentEffortError(value)
diff --git a/src/config/live-reconcile.ts b/src/config/live-reconcile.ts
index d33aa9746b0..b67f70c023b 100644
--- a/src/config/live-reconcile.ts
+++ b/src/config/live-reconcile.ts
@@ -118,6 +118,19 @@ export function adoptPersistedProviderIntoLiveConfig(
if (persistedConfig) refreshPreservedProviderOwner(config, persistedConfig);
}
+/** Adopt only the committed next-start preference; preserve unrelated pending live edits. */
+export function adoptPersistedDesktopCompatibility(config: OcxConfig, persisted: OcxConfig["desktopCompatibility"]): void {
+ const baseline = liveConfigBaseline.get(config);
+ const merged = reconcileConfigValue(baseline?.desktopCompatibility ?? MISSING_CONFIG_VALUE,
+ config.desktopCompatibility ?? MISSING_CONFIG_VALUE, persisted ?? MISSING_CONFIG_VALUE);
+ if (merged === MISSING_CONFIG_VALUE) delete config.desktopCompatibility;
+ else config.desktopCompatibility = merged as OcxConfig["desktopCompatibility"];
+ if (baseline) {
+ if (persisted === undefined) delete baseline.desktopCompatibility;
+ else baseline.desktopCompatibility = structuredClone(persisted);
+ }
+}
+
/** Test seam only: is this instance armed? */
export function claudeCodeBaselineArmed(config: OcxConfig): boolean {
return claudeCodeBaseline.has(config);
diff --git a/src/config/load-degrade.ts b/src/config/load-degrade.ts
index 2c6f160c367..060220f1432 100644
--- a/src/config/load-degrade.ts
+++ b/src/config/load-degrade.ts
@@ -2,6 +2,7 @@ import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
import { chmodSync, existsSync } from "node:fs";
import { join } from "node:path";
import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
+import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
import {
modelPinnedEffortsConfigError,
@@ -123,6 +124,7 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
*/
export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
+ if (desktopCompatibilityConfigError(rawParsed)) console.warn("Invalid desktopCompatibility startup preference ignored; no desktop compatibility service will start automatically.");
if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
warnDegradedStreamMode(rawParsed, validated);
warnDegradedCompactionRouting(rawParsed, validated);
diff --git a/src/config/schema/config-schema.ts b/src/config/schema/config-schema.ts
index 0ac3a0c3ed8..23c939c938d 100644
--- a/src/config/schema/config-schema.ts
+++ b/src/config/schema/config-schema.ts
@@ -1,6 +1,7 @@
import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
import * as z from "zod/v4";
import { compactionRecoverySchema } from "./compaction-recovery";
+import { desktopCompatibilitySchema } from "./desktop-compatibility";
import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
import {
agentTaskRecoverySchema,
@@ -170,6 +171,7 @@ export const configSchema = z.object({
modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
compactionRouting: compactionRoutingSchema.optional().catch(undefined),
compactionRecovery: compactionRecoverySchema.optional().catch(undefined),
+ desktopCompatibility: desktopCompatibilitySchema.optional().catch(undefined),
// A hand-edited malformed phase disables only that phase instead of rejecting
// providers/apiKeys, matching the load-time degradation notice; the management write
// boundary (validateConfigCandidate) still refuses the bad value through the shared,
diff --git a/src/config/schema/desktop-compatibility.ts b/src/config/schema/desktop-compatibility.ts
new file mode 100644
index 00000000000..492404de717
--- /dev/null
+++ b/src/config/schema/desktop-compatibility.ts
@@ -0,0 +1,9 @@
+import * as z from "zod/v4";
+/** Startup resumes observation only. Certificate trust, app launch and Apply are never saved here. */
+export const desktopCompatibilitySchema = z.object({ startOnProxyStart: z.boolean() }).strict();
+export function desktopCompatibilityConfigError(value: unknown): string | null {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
+ const setting = (value as Record).desktopCompatibility;
+ return setting === undefined || desktopCompatibilitySchema.safeParse(setting).success
+ ? null : "schema_invalid: desktopCompatibility: requires only boolean startOnProxyStart";
+}
diff --git a/src/lib/desktop-proxy-route.ts b/src/lib/desktop-proxy-route.ts
new file mode 100644
index 00000000000..fbb84fd5a65
--- /dev/null
+++ b/src/lib/desktop-proxy-route.ts
@@ -0,0 +1,30 @@
+import { configuredOutboundFetch, effectiveProxyFor, noProxyMatches, type ProxyEnvMap, type ProxyCapableRequestInit } from "./proxy-env";
+
+/** Bind both native desktop transports to one explicit route; never fall back after failure. */
+export function desktopProxyFor(url: URL, env: ProxyEnvMap = process.env): string | false {
+ if (noProxyMatches(url, env)) return false;
+ const key = url.protocol === "https:" ? "HTTPS_PROXY" : "HTTP_PROXY";
+ let selected = effectiveProxyFor(url, env);
+ // Unlike an implicit Bun route, this transport explicitly binds HTTP(S) CONNECT.
+ // A malformed scheme-specific setting still refuses instead of falling through.
+ if (!selected && url.protocol === "https:" && !(env[key]?.trim() || env[key.toLowerCase()]?.trim())) {
+ const all = env.ALL_PROXY?.trim() || env.all_proxy?.trim();
+ if (all) {
+ let parsed: URL; try { parsed = new URL(all); } catch { throw new Error("desktop_egress_proxy_invalid"); }
+ if (["http:", "https:"].includes(parsed.protocol)) selected = all;
+ }
+ }
+ if (selected) {
+ const proxy = new URL(selected);
+ if (!["http:", "https:", "socks5:", "socks5h:"].includes(proxy.protocol) || !proxy.hostname
+ || proxy.search || proxy.hash || proxy.pathname !== "" && proxy.pathname !== "/") throw new Error("desktop_egress_proxy_invalid");
+ return selected;
+ }
+ // An explicit but unsupported route must not quietly become direct desktop traffic.
+ if ([env[key], env[key.toLowerCase()], env.ALL_PROXY, env.all_proxy].some(value => value?.trim())) throw new Error("desktop_egress_proxy_invalid");
+ return false;
+}
+export function desktopOutboundFetch(input: RequestInfo | URL, init?: RequestInit): Promise {
+ const url = new URL(input instanceof Request ? input.url : String(input));
+ return configuredOutboundFetch(input, { ...init, proxy: desktopProxyFor(url) } as ProxyCapableRequestInit);
+}
diff --git a/src/lib/desktop-upstream-tunnel.ts b/src/lib/desktop-upstream-tunnel.ts
new file mode 100644
index 00000000000..936ccb0b2f3
--- /dev/null
+++ b/src/lib/desktop-upstream-tunnel.ts
@@ -0,0 +1,107 @@
+import { connect as tcpConnect, isIP, type Socket } from "node:net";
+import { checkServerIdentity, connect as tlsConnect, type TLSSocket } from "node:tls";
+import { desktopProxyFor } from "./desktop-proxy-route";
+import { socks5Credentials, socks5Handshake } from "./socks5-handshake";
+
+// Shares the SOCKS exchange extracted by lcxhh521 in PR #5947 (efdccdbfac3f).
+// Lifecycle here additionally bounds the entire dial and handles close/abort/header limits.
+export interface DesktopTunnelOptions {
+ proxy?: string | false;
+ signal?: AbortSignal;
+ timeoutMs?: number;
+ /** Isolated test peers; production always uses verified TLS to chatgpt.com:443. */
+ target?: { host: string; port: number };
+ ca?: string;
+ proxyCa?: string;
+}
+const fail = () => new Error("desktop_upstream_connection_failed");
+class HandshakeReader {
+ private buffer: Buffer = Buffer.alloc(0);
+ private failure: Error | null = null;
+ private waiting: { ready: () => boolean; resolve: () => void; reject: (error: Error) => void } | null = null;
+ private onData = (chunk: Buffer) => {
+ if (this.buffer.length + chunk.length > 65536) { this.onFailure(); return; }
+ this.buffer = Buffer.concat([this.buffer, chunk]);
+ if (this.waiting?.ready()) { const current = this.waiting; this.waiting = null; current.resolve(); }
+ };
+ private onFailure = () => { this.failure = fail(); const current = this.waiting; this.waiting = null; current?.reject(this.failure); };
+ constructor(private socket: Socket, private signal: AbortSignal) {
+ socket.on("data", this.onData); socket.once("error", this.onFailure); socket.once("end", this.onFailure); socket.once("close", this.onFailure);
+ signal.addEventListener("abort", this.onFailure, { once: true }); if (signal.aborted) this.onFailure(); socket.resume();
+ }
+ write(bytes: Uint8Array | string): void { if (this.failure) throw this.failure; this.socket.write(bytes); }
+ private async until(ready: () => boolean): Promise {
+ if (this.failure) throw this.failure;
+ if (ready()) return;
+ if (this.waiting) throw fail();
+ await new Promise((resolve, reject) => { this.waiting = { ready, resolve, reject }; });
+ }
+ private take(length: number): Buffer { const out = this.buffer.subarray(0, length); this.buffer = this.buffer.subarray(length); return out; }
+ async readExact(length: number): Promise { await this.until(() => this.buffer.length >= length); return this.take(length); }
+ async readHead(): Promise { await this.until(() => this.buffer.indexOf("\r\n\r\n") >= 0); return this.take(this.buffer.indexOf("\r\n\r\n") + 4).toString("latin1"); }
+ dispose(): void {
+ this.socket.pause(); this.socket.removeListener("data", this.onData); this.socket.removeListener("error", this.onFailure);
+ this.socket.removeListener("end", this.onFailure); this.socket.removeListener("close", this.onFailure);
+ this.signal.removeEventListener("abort", this.onFailure);
+ if (this.buffer.length) this.socket.unshift(this.buffer); this.buffer = Buffer.alloc(0); this.onFailure();
+ }
+}
+function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise {
+ return new Promise((resolve, reject) => {
+ const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
+ const done = () => { dispose(); resolve(); };
+ const failure = () => { dispose(); reject(fail()); };
+ socket.once(event, done); socket.once("error", failure); socket.once("close", failure);
+ signal.addEventListener("abort", failure, { once: true }); if (signal.aborted) failure();
+ });
+}
+/** Fixed-destination TLS dial for native desktop upgraded sockets. No logs or direct fallback. */
+export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): Promise {
+ const timeout = options.timeoutMs ?? 10000;
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 30000) throw fail();
+ const cancel = new AbortController(), timer = setTimeout(() => cancel.abort(), timeout);
+ const signal = options.signal ? AbortSignal.any([options.signal, cancel.signal]) : cancel.signal;
+ const sockets = new Set();
+ const own = (socket: T): T => { sockets.add(socket); socket.on("error", () => {}); return socket; };
+ const destroy = () => { for (const socket of sockets) socket.destroy(); };
+ signal.addEventListener("abort", destroy, { once: true });
+ try {
+ if (signal.aborted) throw fail();
+ const proxy = options.proxy === undefined ? desktopProxyFor(new URL("https://chatgpt.com")) : options.proxy;
+ const target = options.target ?? { host: "chatgpt.com", port: 443 };
+ let raw: Socket;
+ if (proxy === false) {
+ raw = own(tcpConnect(target)); await waitConnected(raw, "connect", signal);
+ } else {
+ const url = new URL(proxy), protocol = url.protocol;
+ if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
+ const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
+ raw = own(tcpConnect({ host, port })); await waitConnected(raw, "connect", signal);
+ if (protocol === "https:") {
+ raw = own(tlsConnect({ socket: raw, servername: isIP(host) ? undefined : host, ca: options.proxyCa,
+ rejectUnauthorized: true, checkServerIdentity: (_name, certificate) => checkServerIdentity(host, certificate), ALPNProtocols: ["http/1.1"] }));
+ await waitConnected(raw, "secureConnect", signal);
+ }
+ const reader = new HandshakeReader(raw, signal);
+ try {
+ if (protocol.startsWith("socks")) await socks5Handshake(reader, target, socks5Credentials(url));
+ else {
+ const authority = `${target.host}:${target.port}`;
+ const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`];
+ if (url.username || url.password) lines.push(`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`);
+ reader.write(lines.join("\r\n") + "\r\n\r\n");
+ if (!/^HTTP\/1\.[01] 2\d\d(?: |\r)/.test(await reader.readHead())) throw fail();
+ }
+ } finally { reader.dispose(); }
+ }
+ if (signal.aborted || raw.destroyed) throw fail();
+ const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
+ // The CONNECT reader paused its stream before handing buffered bytes back. Bun's
+ // TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
+ raw.resume();
+ await waitConnected(secured, "secureConnect", signal);
+ if (signal.aborted) throw fail();
+ return secured;
+ } catch { destroy(); throw fail(); }
+ finally { clearTimeout(timer); signal.removeEventListener("abort", destroy); }
+}
diff --git a/src/lib/socks5-fetch.ts b/src/lib/socks5-fetch.ts
index 52bdc3d3b50..3caec90bb16 100644
--- a/src/lib/socks5-fetch.ts
+++ b/src/lib/socks5-fetch.ts
@@ -1,6 +1,7 @@
import net, { type Socket } from "node:net";
import tls, { type TLSSocket } from "node:tls";
import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
+import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
const DEFAULT_SOCKS5_PORT = 1080;
const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
const SOCKS5_VERSION = 0x05;
-const SOCKS5_NO_AUTH = 0x00;
-const SOCKS5_USER_PASS = 0x02;
-const SOCKS5_CONNECT = 0x01;
-const SOCKS5_DOMAIN = 0x03;
-const SOCKS5_SUCCESS = 0x00;
const CRLF = Buffer.from("\r\n");
const HEADER_END = Buffer.from("\r\n\r\n");
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
override readonly name = "Socks5FetchError";
}
-function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
- if (!proxy.username && !proxy.password) return {};
- let username: string;
- let password: string;
- try {
- username = decodeURIComponent(proxy.username);
- password = decodeURIComponent(proxy.password);
- } catch {
- throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
- }
- const usernameBytes = new TextEncoder().encode(username);
- const passwordBytes = new TextEncoder().encode(password);
- if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
- throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
- }
- return { username: usernameBytes, password: passwordBytes };
-}
-
function validateProxy(proxy: string): URL {
let parsed: URL;
try {
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
throw new Socks5FetchError("SOCKS5 proxy port is invalid");
}
if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
- proxyCredentials(parsed);
+ try { socks5Credentials(parsed); } catch (error) {
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
+ throw error;
+ }
return parsed;
}
@@ -130,6 +111,14 @@ class SocketReader {
socket.resume();
}
+ write(bytes: Uint8Array): void {
+ this.socket.write(bytes);
+ }
+
+ readExact(bytes: number, signal?: AbortSignal): Promise {
+ return this.read(bytes, signal);
+ }
+
private readonly onData = (chunk: Buffer | string): void => {
const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
if (this.anyWaiters.length > 0) {
@@ -263,7 +252,7 @@ class SocketReader {
async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise {
const parsedProxy = validateProxy(proxy);
- const credentials = proxyCredentials(parsedProxy);
+ const credentials = socks5Credentials(parsedProxy);
const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
});
const reader = new SocketReader(socket);
try {
- const methods = credentials.username ? Buffer.from([SOCKS5_NO_AUTH, SOCKS5_USER_PASS]) : Buffer.from([SOCKS5_NO_AUTH]);
- socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
- const greeting = await reader.read(2, signal);
- if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
- if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
- socket.write(Buffer.from([
- 0x01,
- credentials.username.byteLength,
- ...credentials.username,
- credentials.password.byteLength,
- ...credentials.password,
- ]));
- const auth = await reader.read(2, signal);
- if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
- } else if (greeting[1] !== SOCKS5_NO_AUTH) {
- throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
- }
-
- const hostname = new TextEncoder().encode(target.hostname);
- if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
- const port = targetPort(target);
- socket.write(Buffer.from([
- SOCKS5_VERSION,
- SOCKS5_CONNECT,
- 0x00,
- SOCKS5_DOMAIN,
- hostname.byteLength,
- ...hostname,
- port >> 8,
- port & 0xff,
- ]));
- const reply = await reader.read(4, signal);
- if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
- if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
- if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
- throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
- }
- const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
- await reader.read(addressLength + 2, signal);
+ await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
socket.setTimeout(0);
return socket;
} catch (error) {
socket.destroy();
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
throw error;
} finally {
reader.dispose();
diff --git a/src/lib/socks5-handshake.ts b/src/lib/socks5-handshake.ts
new file mode 100644
index 00000000000..a917af02e00
--- /dev/null
+++ b/src/lib/socks5-handshake.ts
@@ -0,0 +1,112 @@
+/**
+ * The SOCKS5 handshake both raw outbound transports share: method negotiation
+ * (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
+ * CONNECT exchange.
+ *
+ * `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
+ * `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
+ * dial) each own their socket, their timeouts, and what happens after CONNECT;
+ * this module only frames bytes through the reader each hands it, so a
+ * credentialed `socks5://` proxy URL authenticates identically whether the caller
+ * is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
+ * greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
+ * proxy working for a caller that configured more than it needed.
+ */
+
+export class Socks5HandshakeError extends Error {
+ override readonly name = "Socks5HandshakeError";
+}
+
+const SOCKS5_VERSION = 0x05;
+const SOCKS5_NO_AUTH = 0x00;
+const SOCKS5_USER_PASS = 0x02;
+const SOCKS5_CONNECT = 0x01;
+const SOCKS5_DOMAIN = 0x03;
+const SOCKS5_SUCCESS = 0x00;
+
+/** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
+export interface Socks5Credentials {
+ username?: Uint8Array;
+ password?: Uint8Array;
+}
+
+export function socks5Credentials(proxy: URL): Socks5Credentials {
+ if (!proxy.username && !proxy.password) return {};
+ let username: string;
+ let password: string;
+ try {
+ username = decodeURIComponent(proxy.username);
+ password = decodeURIComponent(proxy.password);
+ } catch {
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
+ }
+ const usernameBytes = new TextEncoder().encode(username);
+ const passwordBytes = new TextEncoder().encode(password);
+ if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
+ }
+ return { username: usernameBytes, password: passwordBytes };
+}
+
+/**
+ * The slice of a transport's socket reader the handshake needs: exact-length reads
+ * that reject on close, on error, or on the transport's own timeout, plus writes.
+ * Bytes read past a step stay queued in the transport's reader; the handshake never
+ * touches the socket itself.
+ */
+export interface Socks5HandshakeReader {
+ write(bytes: Uint8Array): void;
+ readExact(bytes: number, signal?: AbortSignal): Promise;
+}
+
+export interface Socks5Target {
+ host: string;
+ port: number;
+}
+
+/** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
+export async function socks5Handshake(
+ reader: Socks5HandshakeReader,
+ target: Socks5Target,
+ credentials: Socks5Credentials,
+ signal?: AbortSignal,
+): Promise {
+ const hostBytes = Buffer.from(target.host, "utf8");
+ if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
+ const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
+ reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
+ const greeting = await reader.readExact(2, signal);
+ if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
+ if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
+ reader.write(Buffer.from([
+ 0x01,
+ credentials.username.byteLength,
+ ...credentials.username,
+ credentials.password.byteLength,
+ ...credentials.password,
+ ]));
+ const auth = await reader.readExact(2, signal);
+ if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
+ } else if (greeting[1] !== SOCKS5_NO_AUTH) {
+ throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
+ }
+
+ reader.write(Buffer.from([
+ SOCKS5_VERSION,
+ SOCKS5_CONNECT,
+ 0x00,
+ SOCKS5_DOMAIN,
+ hostBytes.byteLength,
+ ...hostBytes,
+ target.port >> 8,
+ target.port & 0xff,
+ ]));
+ const reply = await reader.readExact(4, signal);
+ if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
+ if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
+ if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
+ throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
+ }
+ const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
+ await reader.readExact(addressLength + 2, signal);
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index bc827af06fe..f9632634e85 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,4 +1,5 @@
import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
+import { scheduleDesktopCompatibilityStartup } from "./index/desktop-compatibility-startup";
import { AuxiliaryListenerBindError } from "./ports";
import { runAdmittedBodyWork } from "./inbound-body-admission";
import {
@@ -198,7 +199,7 @@ import { createReadinessGate, type ReadinessGate } from "./readiness";
import { createServeOptions, type ServerIngress } from "./index/serve-options";
import { createOptionalListenerSet, LINK_INGRESS_HOSTNAME } from "./index/optional-listeners";
import { createPackageTreeIntegrityGuardForServer } from "./index/package-tree-guard";
-import { inspectStartupOwnership, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
+import { inspectStartupOwnership, logProxyEndpoints, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
import { acquireSpendLedgerServerLifecycle, recordFailedStartRollback, type SpendLedgerServerLifecycle } from "./index/spend-ledger-lifecycle";
export { waitForFailedStartRollback } from "./index/spend-ledger-lifecycle";
@@ -638,11 +639,14 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
let unregisterQuotaAutoRefresh: (() => void) | null = null;
let remoteWorkspaceStopping = false;
let remoteWorkspaceShutdown: (() => Promise) | undefined;
+ let desktopCompatibilityStartup: ReturnType | undefined;
+ let desktopCompatibilityShutdown: (() => Promise) | undefined;
const managementApiDeps: ManagementApiDeps = {
...deps.managementApi,
listLowQuotaEvents: limit => backgroundLifecycle?.listLowQuotaEvents(limit) ?? [],
remoteWorkspaceStopping: () => remoteWorkspaceStopping,
onRemoteWorkspaceShutdown: shutdown => { remoteWorkspaceShutdown = shutdown; }, linkSupervisor: () => optionalListeners.linkSupervisor(), linkListener: () => optionalListeners,
+ onDesktopCompatibilityShutdown: shutdown => { desktopCompatibilityShutdown = shutdown; },
};
let workspaceRuntimeFlight: Promise | undefined;
const loadRemoteWorkspaceRuntime = () => {
@@ -771,6 +775,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
: []),
() => optionalListeners.stop(),
async () => { await remoteWorkspaceShutdown?.(); },
+ async () => { await desktopCompatibilityStartup?.shutdown(); await desktopCompatibilityShutdown?.(); },
async () => {
try {
userCostOverlayReconciler?.stop();
@@ -800,13 +805,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
boundPort = actualPort;
setCorsOrigin(actualPort);
- console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
- console.log(` POST /v1/responses → provider translation`);
- console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
- console.log(` GET /healthz → health check`);
- console.log(` GET /api/* → management API`);
- console.log(` GET / → GUI dashboard`);
-
+ logProxyEndpoints(actualPort);
if (loopbackServer) {
// Loud on every start, not once at enable time. An operator who inherits a config, or
// who forgot, has to be able to see that an unauthenticated surface is live without
@@ -878,6 +877,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
}
startPackageRefresh();
+ desktopCompatibilityStartup = scheduleDesktopCompatibilityStartup(config, { boundHostname: bindHost, boundPort: actualPort, loopbackPort: loopbackServer?.port ?? undefined, readiness: deps.readinessGate });
return server;
}
diff --git a/src/server/index/desktop-compatibility-startup.ts b/src/server/index/desktop-compatibility-startup.ts
new file mode 100644
index 00000000000..a80bf1f5d58
--- /dev/null
+++ b/src/server/index/desktop-compatibility-startup.ts
@@ -0,0 +1,56 @@
+import type { OcxConfig } from "../../types";
+import { siblingOfLivePort } from "../../codex/sibling-start";
+import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
+import type { DesktopCompatibilityRuntime } from "../../codex/desktop-compatibility/runtime";
+import { bindNativeCompatibilityOwner } from "../../codex/desktop-compatibility/routing-binding";
+import type { ReadinessGate } from "../readiness";
+
+type RuntimeModule = { getDesktopCompatibilityRuntime(): DesktopCompatibilityRuntime; shutdownDesktopCompatibility(): Promise };
+interface StartupIo {
+ platform?: string;
+ testGuard?: boolean;
+ sibling?: boolean;
+ load?: () => Promise;
+ warn?: (message: string) => void;
+ boundPort?: number;
+ boundHostname?: string;
+ loopbackPort?: number;
+ readiness?: Pick;
+ readinessTimeoutMs?: number;
+}
+/** Core-safe gate: off installs do not load the optional runtime, read credentials or start timers. */
+export function scheduleDesktopCompatibilityStartup(config: OcxConfig, io: StartupIo = {}): { shutdown(): Promise } {
+ let stopped = false, module: RuntimeModule | undefined;
+ let cancelReadiness: (() => void) | undefined;
+ const waitForReadiness = () => !io.readiness ? Promise.resolve(true) : new Promise(resolve => {
+ const deadline = Date.now() + (io.readinessTimeoutMs ?? 120000);
+ let timer: ReturnType | undefined;
+ const finish = (ready: boolean) => { if (timer) clearTimeout(timer); cancelReadiness = undefined; resolve(ready); };
+ cancelReadiness = () => finish(false);
+ const check = () => {
+ const status = io.readiness!.getStatus();
+ if (stopped || status !== "pending" || Date.now() >= deadline) { finish(!stopped && status === "ready"); return; }
+ timer = setTimeout(check, 100); timer.unref();
+ };
+ check();
+ });
+ const unbind = io.boundPort === undefined || io.boundHostname === undefined ? () => {} : bindNativeCompatibilityOwner({ config, hostname: io.boundHostname, port: io.boundPort, loopbackPort: io.loopbackPort });
+ const enabled = config.desktopCompatibility?.startOnProxyStart === true && (io.platform ?? process.platform) === "win32"
+ && !(io.testGuard ?? isTestHomeGuardArmed()) && !(io.sibling ?? siblingOfLivePort() !== null)
+ && config.runtimeRole !== "client";
+ const pending = enabled ? Promise.resolve().then(async () => {
+ if (stopped) return;
+ if (io.readiness) {
+ const ready = await waitForReadiness();
+ if (stopped) return;
+ if (!ready) throw new Error("desktop_compatibility_startup_not_ready");
+ }
+ module = await (io.load?.() ?? import("../../codex/desktop-compatibility/service"));
+ if (stopped) return;
+ await module.getDesktopCompatibilityRuntime().start();
+ }).catch(() => { (io.warn ?? console.warn)("Desktop compatibility observation did not start. Inspect Desktop compatibility status; no automatic trust or correction was applied."); }) : Promise.resolve();
+ return { async shutdown() {
+ stopped = true; cancelReadiness?.(); unbind(); await pending;
+ if (module) await module.shutdownDesktopCompatibility();
+ } };
+}
diff --git a/src/server/index/startup-warnings.ts b/src/server/index/startup-warnings.ts
index 5ebbb04805e..ec49b184bc4 100644
--- a/src/server/index/startup-warnings.ts
+++ b/src/server/index/startup-warnings.ts
@@ -258,3 +258,12 @@ export function warnPlaintextV2AgentMessagesStartup(config: { plaintextV2AgentMe
console.warn(" Eligible ChatGPT collaboration calls may carry plaintext message arguments. HTTPS remains encrypted, but task text may be retained in Codex history, selected providers, and local response/debug state.");
console.warn(" This depends on undocumented ChatGPT and Codex behavior; it does not decrypt existing tasks.");
}
+
+export function logProxyEndpoints(actualPort: number): void {
+ console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
+ console.log(` POST /v1/responses → provider translation`);
+ console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
+ console.log(` GET /healthz → health check`);
+ console.log(` GET /api/* → management API`);
+ console.log(` GET / → GUI dashboard`);
+}
diff --git a/src/server/management-api.ts b/src/server/management-api.ts
index 1f6b28e427d..7dfa9ddd786 100644
--- a/src/server/management-api.ts
+++ b/src/server/management-api.ts
@@ -79,6 +79,9 @@ import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
import { handleIntegrationRoutes } from "./management/integration-routes";
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
import { handleClaudeDesktopPickerRoutes } from "./management/claude-desktop-picker-routes";
+import { handleDesktopCompatibilityRoutes } from "./management/desktop-compatibility-routes";
+import { handleDesktopCompatibilityRuntimeRoutes } from "./management/desktop-compatibility-runtime-routes";
+import { handleDesktopCompatibilitySettingsRoutes } from "./management/desktop-compatibility-settings-routes";
import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
import type { ManagementContext } from "./management/context";
import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
@@ -358,6 +361,9 @@ export async function handleManagementAPI(
?? (await handleNativeIntegrationRoutes(ctx))
?? (await handleCursorIntegrationRoutes(ctx))
?? (await handleClaudeDesktopPickerRoutes(ctx))
+ ?? (await handleDesktopCompatibilityRoutes(ctx))
+ ?? (await handleDesktopCompatibilityRuntimeRoutes(ctx))
+ ?? (await handleDesktopCompatibilitySettingsRoutes(ctx))
?? (await handleAgentSettingsRoutes(ctx))
?? (await handleCodexPromptRoutes(ctx))
?? (await handleOauthAccountRoutes(ctx))
diff --git a/src/server/management/context.ts b/src/server/management/context.ts
index 9162b855034..39ee5b63cd5 100644
--- a/src/server/management/context.ts
+++ b/src/server/management/context.ts
@@ -45,6 +45,10 @@ export interface ManagementRequestIngress {
}
export interface ManagementApiDeps {
+ desktopStartupSettings?: import("../../codex/desktop-compatibility/startup-settings").DesktopStartupSettingsService;
+ desktopCertificateService?: import("../../codex/desktop-compatibility/certificate-service").DesktopCertificateService;
+ desktopCompatibilityRuntime?: import("../../codex/desktop-compatibility/runtime").DesktopCompatibilityRuntime;
+ onDesktopCompatibilityShutdown?: (shutdown: () => Promise) => void;
/** Bound to this server's lifecycle owner; absent in direct route tests. */
listLowQuotaEvents?: (limit?: number) => LowQuotaEvent[];
/** Bound Claude intercept state, injectable for isolated management-route tests. */
diff --git a/src/server/management/desktop-compatibility-routes.ts b/src/server/management/desktop-compatibility-routes.ts
new file mode 100644
index 00000000000..af67e429a51
--- /dev/null
+++ b/src/server/management/desktop-compatibility-routes.ts
@@ -0,0 +1,46 @@
+import { jsonResponse } from "../auth-cors";
+import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
+import type { ManagementContext } from "./context";
+import type { DesktopCertificateService } from "../../codex/desktop-compatibility/certificate-service";
+
+const PATH = "/api/codex/desktop-compatibility/certificate";
+let service: DesktopCertificateService | undefined;
+
+export async function handleDesktopCompatibilityRoutes(ctx: ManagementContext): Promise {
+ if (ctx.url.pathname !== PATH) return null;
+ if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
+ // A local dashboard confirmation precedes any persisted key or CurrentUser Root mutation.
+ // This is caller provenance, not protection against an arbitrary same-user process.
+ if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) {
+ return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
+ }
+ let action: "prepare" | "trust" | "remove-trust" | "renew" | undefined, fingerprint: string | undefined;
+ if (ctx.req.method === "POST") {
+ let body: unknown;
+ try { body = await readManagementJsonBody(ctx.req); }
+ catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
+ if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
+ const value = body as Record;
+ if (Object.keys(value).some(key => !["action", "fingerprint", "confirmed"].includes(key)) || value.confirmed !== true
+ || typeof value.action !== "string" || !["prepare", "trust", "remove-trust", "renew"].includes(value.action)) return jsonResponse({ error: "invalid_request" }, 400);
+ action = value.action as typeof action;
+ if (action !== "prepare" && (typeof value.fingerprint !== "string" || !/^[A-F0-9]{64}$/.test(value.fingerprint))) {
+ return jsonResponse({ error: "certificate_fingerprint_required" }, 400);
+ }
+ if (action === "prepare" && value.fingerprint !== undefined) return jsonResponse({ error: "unexpected_fingerprint" }, 400);
+ fingerprint = value.fingerprint as string | undefined;
+ }
+ const controller = ctx.deps.desktopCertificateService ?? (service ??= (await import("../../codex/desktop-compatibility/certificate-service")).createDesktopCertificateService());
+ try {
+ const status = action === "prepare" ? await controller.prepare()
+ : action === "trust" ? await controller.trust(fingerprint!)
+ : action === "renew" ? await controller.renew(fingerprint!)
+ : action === "remove-trust" ? await controller.removeTrust(fingerprint!) : await controller.status();
+ return jsonResponse({ ok: true, certificate: status });
+ } catch (error) {
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "operation_failed";
+ const allowed = new Set(["unsupported", "busy", "not_prepared", "fingerprint_changed", "app_running", "runtime_running", "app_state_unknown", "trust_unknown", "trust_not_applied",
+ "unsafe_path", "unreadable", "expired", "renewal_required", "protection_failed"]);
+ return jsonResponse({ ok: false, error: allowed.has(code) ? code : "operation_failed" }, 409);
+ }
+}
diff --git a/src/server/management/desktop-compatibility-runtime-routes.ts b/src/server/management/desktop-compatibility-runtime-routes.ts
new file mode 100644
index 00000000000..eeb4bbe2b47
--- /dev/null
+++ b/src/server/management/desktop-compatibility-runtime-routes.ts
@@ -0,0 +1,46 @@
+import { jsonResponse } from "../auth-cors";
+import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
+import type { ManagementContext } from "./context";
+import type { DesktopCompatibilityRuntime } from "../../codex/desktop-compatibility/runtime";
+
+const PATH = "/api/codex/desktop-compatibility/runtime";
+const ERROR_CODES = new Set(["busy", "unsupported", "test_environment", "stopping", "build_unverified", "egress_proxy_invalid",
+ "trust_required", "certificate_expiring", "certificate_not_prepared", "certificate_invalid", "certificate_expired",
+ "native_identity_unverified", "native_routing_unverified", "cleanup_incomplete", "not_running", "connection_invalid", "connection_changed", "connection_cleanup_required", "connection_unavailable"]);
+
+export async function handleDesktopCompatibilityRuntimeRoutes(ctx: ManagementContext): Promise {
+ if (ctx.url.pathname !== PATH) return null;
+ if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
+ if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
+ let action: "start" | "stop" | "observe" | "apply" | "launch" | undefined;
+ if (ctx.req.method === "POST") {
+ let body: unknown;
+ try { body = await readManagementJsonBody(ctx.req); }
+ catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
+ if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
+ const value = body as Record;
+ if (Object.keys(value).some(key => !["action", "confirmed", "accountWideConsent"].includes(key)) || value.confirmed !== true
+ || typeof value.action !== "string" || !["start", "stop", "observe", "apply", "launch"].includes(value.action)) return jsonResponse({ error: "invalid_request" }, 400);
+ if (value.action === "apply" ? value.accountWideConsent !== true : value.accountWideConsent !== undefined) return jsonResponse({ error: "invalid_consent_scope" }, 400);
+ action = value.action as typeof action;
+ }
+ const module = ctx.deps.desktopCompatibilityRuntime ? null : await import("../../codex/desktop-compatibility/service");
+ const service: DesktopCompatibilityRuntime = ctx.deps.desktopCompatibilityRuntime ?? module!.getDesktopCompatibilityRuntime();
+ if (module) ctx.deps.onDesktopCompatibilityShutdown?.(module.shutdownDesktopCompatibility);
+ try {
+ if (action === "apply") {
+ const result = await service.apply(true);
+ return jsonResponse({ ok: result.accepted, activation: result, runtime: service.status() }, result.accepted ? 202 : 409);
+ }
+ if (action === "launch") {
+ const result = await service.launch();
+ return jsonResponse({ ok: result.status === "started", launch: result, runtime: service.status() }, result.status === "started" ? 200 : 409);
+ }
+ const result = action === "start" ? await service.start() : action === "stop" ? await service.stop()
+ : action === "observe" ? await service.observe() : service.status();
+ return jsonResponse({ ok: true, runtime: result });
+ } catch (error) {
+ const code = error instanceof Error ? error.message.replace(/^desktop_compatibility_/, "") : "operation_failed";
+ return jsonResponse({ ok: false, error: ERROR_CODES.has(code) ? code : "operation_failed", runtime: service.status() }, 409);
+ }
+}
diff --git a/src/server/management/desktop-compatibility-settings-routes.ts b/src/server/management/desktop-compatibility-settings-routes.ts
new file mode 100644
index 00000000000..b051292673f
--- /dev/null
+++ b/src/server/management/desktop-compatibility-settings-routes.ts
@@ -0,0 +1,27 @@
+import { jsonResponse } from "../auth-cors";
+import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
+import type { ManagementContext } from "./context";
+
+const PATH = "/api/codex/desktop-compatibility/settings";
+export async function handleDesktopCompatibilitySettingsRoutes(ctx: ManagementContext): Promise {
+ if (ctx.url.pathname !== PATH) return null;
+ if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
+ if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
+ let update: { startOnProxyStart: boolean; revision: string } | undefined;
+ if (ctx.req.method === "POST") {
+ let body: unknown;
+ try { body = await readManagementJsonBody(ctx.req); }
+ catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
+ if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
+ const value = body as Record;
+ if (Object.keys(value).some(key => !["startOnProxyStart", "revision", "confirmed"].includes(key)) || value.confirmed !== true
+ || typeof value.startOnProxyStart !== "boolean" || typeof value.revision !== "string" || !/^[a-f0-9]{64}$/.test(value.revision)) return jsonResponse({ error: "invalid_request" }, 400);
+ update = { startOnProxyStart: value.startOnProxyStart, revision: value.revision };
+ }
+ const service = ctx.deps.desktopStartupSettings ?? (await import("../../codex/desktop-compatibility/startup-settings")).createDesktopStartupSettings({ liveConfig: ctx.config });
+ try { return jsonResponse({ ok: true, settings: update ? service.set(update.startOnProxyStart, update.revision) : service.status() }); }
+ catch (error) {
+ const code = error instanceof Error ? error.message.replace(/^desktop_compatibility_settings_/, "") : "operation_unconfirmed";
+ return jsonResponse({ ok: false, error: ["unavailable", "invalid", "changed", "invalid_request"].includes(code) ? `settings_${code}` : "settings_operation_unconfirmed" }, 409);
+ }
+}
diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts
index f2441b824da..240e4a3c0b4 100644
--- a/src/server/management/route-registry.ts
+++ b/src/server/management/route-registry.ts
@@ -163,6 +163,12 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
// server/management/claude-desktop-picker-routes
{ method: "GET", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: false },
{ method: "PUT", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: true },
+ { method: "GET", path: "/api/codex/desktop-compatibility/certificate", module: "server/management/desktop-compatibility-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "Certificate status is available over authenticated HTTP while the desktop compatibility runtime/CLI status contract is integrated." } },
+ { method: "POST", path: "/api/codex/desktop-compatibility/certificate", module: "server/management/desktop-compatibility-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Certificate setup requires a confirmed local dashboard session; raw admin tokens cannot enroll or remove OS trust." } },
+ { method: "GET", path: "/api/codex/desktop-compatibility/runtime", module: "server/management/desktop-compatibility-runtime-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "Experimental runtime status is authenticated HTTP while the native compatibility UI and CLI contract are integrated." } },
+ { method: "POST", path: "/api/codex/desktop-compatibility/runtime", module: "server/management/desktop-compatibility-runtime-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Native app launch and account-wide UI trial require explicit local dashboard confirmation." } },
+ { method: "GET", path: "/api/codex/desktop-compatibility/settings", module: "server/management/desktop-compatibility-settings-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "The persisted startup preference is currently owned by the local desktop compatibility panel." } },
+ { method: "POST", path: "/api/codex/desktop-compatibility/settings", module: "server/management/desktop-compatibility-settings-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Saved native observation intent requires an explicit local GUI action and fresh field revision." } },
{ method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
{ method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
{ method: "PUT", path: "/api/grok/selection", module: "server/management/agent-settings-routes", mutates: true },
diff --git a/src/server/management/sibling-guard.ts b/src/server/management/sibling-guard.ts
index a03bb52e740..5f1754eb4f5 100644
--- a/src/server/management/sibling-guard.ts
+++ b/src/server/management/sibling-guard.ts
@@ -30,6 +30,7 @@ export const SIBLING_REFUSED_MANAGEMENT_PATHS: readonly { readonly path: string;
{ path: "/api/client-integrations", children: true },
{ path: "/api/native-integrations", children: true },
{ path: "/api/claude-desktop", children: true },
+ { path: "/api/codex/desktop-compatibility", children: true },
{ path: "/api/claude-code", children: false },
{ path: "/api/grok/apply", children: false },
{ path: "/api/grok/selection", children: false },
diff --git a/src/types/config.ts b/src/types/config.ts
index c771c6299ef..138a9e992ea 100644
--- a/src/types/config.ts
+++ b/src/types/config.ts
@@ -760,6 +760,8 @@ export interface OcxConfig {
};
/** Opt-in failure-only recovery; never replaces the initial compaction model. */
compactionRecovery?: { enabled: boolean; model: string; allowDevinInvalidArgument?: boolean };
+ /** Explicit opt-in; resumes Observe only using an existing trusted certificate and endpoints. */
+ desktopCompatibility?: { startOnProxyStart: boolean };
/**
* Destination model for Codex's own memory pipeline, per phase
* (src/server/responses/memory-models.ts).
diff --git a/structure/INDEX.md b/structure/INDEX.md
index 2f7e3eea32c..42a3db7f04a 100644
--- a/structure/INDEX.md
+++ b/structure/INDEX.md
@@ -81,6 +81,7 @@ The dashboard, the management API, and third-party client config ownership.
| [`gui-and-management-api.md`](gui-and-management-api.md) | Dashboard serving, authentication boundaries, /api/* ownership, and startup safety. |
| [`dashboard-and-usage.md`](dashboard-and-usage.md) | Dashboard page contracts, usage accounting and request metrics, and per-surface management settings. |
| [`clients/integrations.md`](clients/integrations.md) | Third-party client config ownership, snapshots, refresh, disable, and restore. |
+| [`clients/codex-desktop.md`](clients/codex-desktop.md) | Windows package activation, restart context, and protected compatibility authority persistence. |
| [`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md) | Experimental macOS app-server stdout shim, opt-in launch, restore and failure boundaries. |
| [`clients/claude-desktop.md`](clients/claude-desktop.md) | Claude Desktop profile ownership and config-library resolution. |
| [`companion.md`](companion.md) | Shared timeline filtering, usage/quotas, native and web tray title, and WidgetKit display contracts. |
@@ -121,10 +122,10 @@ A source area can be described by more than one doc, because these docs are orga
| `src/chatgpt/` | [`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md) |
| `src/claude/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/cli.ts` | [`runtime.md`](runtime.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
-| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
+| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/codex-desktop.md`](clients/codex-desktop.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/client/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) |
| `src/clients/` | [`clients/integrations.md`](clients/integrations.md) |
-| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
+| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`clients/codex-desktop.md`](clients/codex-desktop.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) |
| `src/combos/` | [`runtime.md`](runtime.md)
[`providers-and-adapters.md`](providers-and-adapters.md)
[`providers/jev-decision.md`](providers/jev-decision.md) |
| `src/companion/` | [`overview.md`](overview.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`companion.md`](companion.md) |
| `src/compatibility/` | [`runtime.md`](runtime.md)
[`adapters/compatibility-contracts.md`](adapters/compatibility-contracts.md) |
diff --git a/structure/clients/codex-desktop.md b/structure/clients/codex-desktop.md
new file mode 100644
index 00000000000..7d45b91b916
--- /dev/null
+++ b/structure/clients/codex-desktop.md
@@ -0,0 +1,217 @@
+# Codex Desktop compatibility
+
+## Explicit restart
+
+`ocx system codex-restart` requests a full Codex app and app-server restart through
+the management endpoint. `src/cli/capabilities.ts` and `src/cli/system-command.ts`
+warn that unsaved drafts, picker selections and pending approvals may be lost.
+The unconfirmed path sends no restart request; JSON output preserves refused outcomes.
+
+Under the test preload's `OCX_TEST_HOME_GUARD=1`,
+`src/codex/desktop-app-restart.ts` skips real restart without an injected executor.
+`src/cli/restart-scope.ts` reports that skip. `NODE_ENV=test` alone does not control
+this boundary. `tests/clients/desktop-app-restart.test.ts` covers the contract.
+
+## Windows launch context
+
+`src/codex/desktop-app/windows.ts` captures an already active loopback compatibility
+PAC from the main package process only when its exact URL is registered by the current
+process-local serving runtime. The runtime publishes a fresh generation after its listeners
+bind and revokes it before cleanup. Disk state and URL shape cannot establish this ownership;
+stopped, expired, foreign or unregistered runtimes refuse capture before termination.
+The adapter rechecks the captured generation immediately before package activation, so even
+a replacement runtime serving identical ports cannot inherit the old restart approval.
+Helpers cannot override it and conflicting main
+processes refuse before termination. No other process arguments are carried forward.
+Captured command lines stay internal, outside restart results and diagnostic logs.
+Capture failures return `relaunch_context_failed` and release the restart lock before any process is signalled; the CLI reports that the app was not stopped.
+An explicitly empty or whitespace-only root command line remains unknown after CIM parsing and refuses context capture; an unreadable helper cannot erase a known root PAC.
+
+`src/codex/desktop-compatibility/windows-package-command.ts` validates the canonical
+loopback URL shape, rechecks the discovered package manifest, activates with
+`IApplicationActivationManager`, and verifies the package identity and actual PAC
+argument. Normal launches keep the existing AppsFolder route. A standalone compatibility
+launch refuses an already running app; it never quits an app or installs a watcher.
+Activation reads the final non-empty JSON line after trimming a BOM or preceding warnings. Invalid output and executor failures remain unverified, without automatic relaunch retries.
+`tests/clients/desktop-compatibility-launch.test.ts` covers restart integration and the
+real Windows parser/COM service without launching the user's app.
+
+## Certificate persistence
+
+`src/codex/desktop-compatibility/certificate-store.ts` is a separately callable store
+for a 30-day authority constrained to `chatgpt.com`, with IP exclusions and a TLS-server-only EKU. An explicit
+feature-owned directory and lifecycle lease protect one atomic envelope. Its public
+certificate is bound to a CurrentUser-DPAPI-protected private payload.
+
+Reopening reuses the same key and fingerprint. Corrupt, foreign-user and expired state
+refuses instead of silently replacing a trusted identity. Renewal is reported within
+the last seven days. The store does not register certificates, start listeners, enable
+compatibility settings or change the running app. A deliberate renewal validates the
+replacement envelope before atomic publication and compares the existing identity again.
+Failure before publication preserves the original removable identity; staging is cleaned.
+An older authority without the server-only EKU reports `renewal-required`: serving and trust
+registration refuse it, while exact trust removal and deliberate renewal remain available.
+
+`src/codex/desktop-compatibility/windows-key-protection.ts` uses trusted PowerShell
+and bounded stdin/stdout, never command-line secrets or plaintext fallback. CurrentUser
+protects against other OS identities, not another process using the same credentials.
+`tests/clients/desktop-compatibility-authority.test.ts` covers persistence, refusal,
+and a real Windows DPAPI round trip with synthetic data.
+
+## Certificate setup API
+
+`src/server/management/desktop-compatibility-routes.ts` exposes the authenticated
+`/api/codex/desktop-compatibility/certificate` setup endpoint. GET is read-only public metadata and OS-trust inspection;
+it does not decrypt a key, create a file, acquire a lease or register trust. POST
+requires a GUI-session principal on trusted loopback ingress and explicit confirmation.
+Trust mutations also require the exact SHA-256 fingerprint. Raw admin tokens cannot
+substitute browser provenance; this does not protect against arbitrary same-user code.
+
+`src/codex/desktop-compatibility/certificate-service.ts` serializes setup and refuses
+busy operations. Only prepare may generate a key. Trust/removal load existing validated
+state, recheck its fingerprint, and never repair missing state by creating a new root.
+Removal refuses while the app is running or its process state cannot be established.
+An expired key is loadable only for removal, not for renewed trust.
+Explicit renewal first proves the app absent and verifies old trust removal, then replaces
+the encrypted envelope. Unknown or refused removal never loses the old key. The new root
+remains untrusted until a separate fingerprint-bound confirmation; renewal never creates
+an automatic trust prompt or accumulates old trusted roots.
+
+`src/codex/desktop-compatibility/windows-certificate-trust.ts` uses the CurrentUser Root
+store and exact certificate bytes. Mutations require the matching private key from the
+protected store, not status metadata. Idempotent actions skip repeated OS changes;
+uncertain command completion is resolved by an independent readback. Unknown readback
+remains unknown. Public responses contain no PEM, private-key objects or subprocess output.
+
+Sibling instances refuse certificate mutations because OS trust is shared user state.
+The registry declares the certificate-status CLI verb as deferred to the desktop
+compatibility integration owner; it currently has an authenticated HTTP contract only.
+
+## Optional compatibility runtime
+
+`src/codex/desktop-compatibility/runtime.ts` owns an explicit, default-off runtime.
+Construction and status do not start listeners, load credentials or enroll trust. Start
+loads an existing DPAPI key, verifies CurrentUser trust and a fresh native file-login
+identity, then creates only loopback TLS/CONNECT/PAC listeners. The currently assessed
+Windows package version is declared in the module. Unknown builds refuse activation.
+The assessed family/publisher, full package basename and App entry must also agree;
+matching the version prefix alone does not qualify a foreign package identity.
+HTTP, identity verification and upgraded sockets use the explicit desktop egress policy
+described in the [transport inventory](../transports/inventory.md#native-desktop-proxy-egress).
+Invalid proxy routes refuse rather than falling back to direct egress.
+Native routing verification binds both the actual listener address family and port; the companion
+is IPv4-only. Ambiguous localhost aliases cannot qualify, and listener identity changes revoke observation.
+
+`relay-listener.ts` forwards HTTP with the existing upstream-header filter, cookies and
+streaming bodies, and pipes upgraded TLS sockets without decoding their frames. The
+upstream is fixed to chatgpt.com; request Host cannot select another destination. CONNECT
+allows only chatgpt.com:443. PAC has a certificate-relative deadline and `DIRECT` fallback.
+HTTP and upgraded requests accept the case-insensitive DNS spelling of that exact Host,
+with optional port443, while other hosts, ports and non-origin request targets remain refused.
+The package launcher uses only the runtime-owned PAC and never kills an existing app.
+
+`connection-store.ts` preserves the PAC nonce and two public loopback ports in a bounded,
+strictly validated `connection.json` beside the protected authority. No account, credential,
+key or expiry is stored there. First publication is create-only under a lifecycle lease;
+another identity cannot be overwritten. Startup binds the recorded ports and revalidates
+publication before exposing a launch URL. A conflict or malformed file fails without new
+port allocation. Existing cached PACs can reconnect after a service restart using the same
+authority and endpoints; correction always restarts in Observe. Certificate renewal still
+requires a closed app, so its next launch fetches the new certificate-relative PAC deadline.
+If both publication and temporary-file cleanup fail, the cleanup-required error retains both causes; cleanup never throws from a `finally` block or reports the residue as removed.
+
+`usage-controller.ts`, `usage-activation.ts` and `usage-policy.ts` implement a maximum
+three-minute, explicitly confirmed account-UI trial after a fresh supported exhaustion
+snapshot. They cannot assert selected-provider isolation. Two usage gate booleans may
+change; quota windows, credits, spending limits and other responses remain original.
+An original available or protected usage record disarms Apply and advances its generation.
+Later exhaustion does not resume that trial; it requires another explicit activation.
+Pending response checks from the previous generation cannot emit a correction after recovery.
+Native identity verification also binds an opaque reader-local credential generation from a stable file-stat/content snapshot. Replacement, token rotation and A-to-B-to-A restoration invalidate pending identity checks and response correction; they require a fresh observation runtime. Neither credential hashes nor tokens appear in public status. `tests/clients/desktop-compatibility-native-identity.test.ts` exercises delayed verification and build-check races with synthetic auth files.
+Fresh identity checks, generation changes, unknown schemas and elapsed deadlines refuse
+correction. `usage-sse-controller.ts` preserves event metadata and original sequence IDs;
+records over its default 256 KiB cap pass through unchanged until the record delimiter,
+after which framing resumes for later records.
+`usage-refresh.ts` closes only usage streams bound by validated original account records.
+`usage-controlled-fetch.ts` removes stale validators from changed JSON and controlled SSE.
+Response production is reported separately from app-cache or UI confirmation.
+Observation counters distinguish validated JSON/SSE snapshots from merely registered streams;
+only identity-bound, untainted active streams count as `validatedActiveStreams`. Counts and the
+last snapshot time describe this relay's lifetime, including responses sent by diagnostic clients.
+They never establish a source PID, authoritative gate coverage or composer recovery. Both
+`sourceProcessVerified` and `composerRecoveryVerified` remain false. Invalid/foreign snapshots do
+not advance these counters. Conversation initialization and other non-WHAM endpoints retain
+their original response bytes, including `blocked_features` and `limits_progress`.
+
+`routing-binding.ts` records the server's actual bound data/companion ports. Shutdown
+unregisters only its matching owner. `routing-preflight.ts` verifies bounded native root
+TOML and any selected root profile against those ports, rejecting foreign providers,
+remote destinations, authless mode, unknown profiles and process-level app overrides.
+The verifier binds parsed root TOML values, actual listener ports and the configured provider/model/fallback
+routing inputs to its first valid observation. A change or failed check invalidates that runtime
+even if the old settings return; stop/start creates a fresh observation context. Unrelated
+OpenCodex preferences, TOML formatting, object-key order and the native root `mcp_servers`
+table do not invalidate the routing snapshot. Desktop refreshes that tool-transport table
+after launch; all other native fields, including unknown ones and selected profiles, remain bound.
+It does not claim knowledge of project-local overrides or a conversation's selected model.
+Each record that would be corrected checks routing and the assessed installed build asynchronously,
+then rechecks account identity and trial generation before emitting it. `installed-build.ts` shares
+the Windows adapter's discovery parser, coalesces concurrent probes without caching a positive
+result across requests, and aborts/reaps its bounded child before shutdown completes. Observe and
+unchanged responses do not query Windows. Background refresh runs once a minute in Observe,
+every ten seconds during Apply, and stops probing after the safety deadline. Failed checks
+disarm Apply. Native update/routing failures are
+public diagnostic codes; originals continue to relay and no app/config repair is automatic.
+
+`runtime-ownership.ts` serializes certificate mutations against active/starting runtimes.
+The existing sibling guard blocks all runtime mutations in sibling instances. Core shutdown
+registration occurs only after successful startup. Cleanup stops owned listeners and streams;
+a failed cleanup retains ownership and reports `cleanup-required`, never a false `off` state.
+
+`src/server/management/desktop-compatibility-runtime-routes.ts` provides GET status and
+local GUI-session POST start/stop/observe/apply/launch, with explicit confirmation and
+separate account-wide consent for apply. No setting, login, quota, automatic startup or
+dashboard preference is changed by this API. The separate startup preference below never saves Apply.
+The status CLI verb remains deferred to this integration owner.
+
+## Dashboard controls
+
+`gui/src/pages/codex-desktop-compatibility.tsx` is a lazy Codex Set tab at
+`#codex-set/desktop`. It uses the machine API base, never the shared hub base. Managed
+OpenCodex client mode does not offer these controls because its local listener deliberately
+does not admit durable machine mutations through a dashboard bootstrap session.
+
+`gui/src/desktop-compatibility-api.ts` projects public status and issues one POST per action.
+Fingerprint-bound trust/renew/removal and the account-wide trial require separate acknowledgement.
+Observe/start/stop/launch follow explicit button actions. No action is replayed after an uncertain
+response; a fresh status read is required. Changing the API target remounts the panel and discards
+pending consent. `useClientResource` owns bounded, visibility-aware reads and invalidates earlier
+reads when a mutation result is published. Certificate status is not repeatedly polled; runtime
+status polls only while the tab is active. Consent copy exists in all ten locale catalogs.
+Trial consent and pending withdrawal distinguish stopped response correction from unconfirmed native cache refresh; the panel never reports cache rollback as confirmed.
+The panel offers renewal only for prepared, trusted, expired or renewal-required identities. Unknown trust permits fingerprint-verified removal but not renewal; invalid keys expose neither action.
+Running-state help distinguishes a listening service from a connected native app: ordinary app launches or updates can omit the managed PAC argument. It points to explicit package launch after the user closes Codex and states that a certificate reinstall cannot authorize an unassessed build.
+
+## Proxy startup preference
+
+`desktopCompatibility.startOnProxyStart` is opt-in and defaults absent/off. The strict schema
+rejects candidate writes containing unknown options, while invalid hand edits disable startup.
+`src/server/index/desktop-compatibility-startup.ts` gates optional imports on this intent,
+Windows, non-test execution and non-sibling/non-client ownership. It keeps `startServer`
+synchronous and does not await before the Lab activation boundary. Unsupported prerequisites
+warn without stopping the model proxy or installing trust.
+When the CLI supplies its readiness gate, observation waits for post-startup native configuration
+sync before loading its runtime. A failed sync or a two-minute pending limit leaves observation
+off with a generic diagnostic; shutdown cancels this optional wait without delaying proxy exit.
+
+`service.ts` shares one runtime between startup and management. Server shutdown retains
+the asynchronous teardown, waits for in-flight startup, and prevents a late start after stop.
+The saved preference does not launch Codex, enroll or renew certificates, or resume a trial.
+The dashboard's startup toggle changes only this next-process preference. Its settings API
+requires local GUI provenance, explicit boolean intent and a revision of the displayed field.
+`startup-settings.ts` uses the existing config mutation lock/rebase writer, then reads back
+the real file. Unrelated concurrent fields survive. `adoptPersistedDesktopCompatibility`
+updates only this field and its live comparison baseline, so a later unrelated save cannot
+undo the committed preference or overwrite newer disk edits. Publication-side errors remain
+errors, with verified disk state adopted rather than speculative rollback or request replay.
+Invalid/missing configuration cannot be recreated by toggling this setting.
diff --git a/structure/config.md b/structure/config.md
index d51f33020c4..d25a0af2d89 100644
--- a/structure/config.md
+++ b/structure/config.md
@@ -28,7 +28,7 @@ the [source-owned credential contract](codex-home.md#orca-source-owned-account-i
## Config surface
-Custom Responses providers can explicitly enable `preserveResponsesInputItemIds` and `preserveResponsesMessageMetadata`; both default off and accept only booleans. The [replay sanitization contract](transports/responses-wire-shapes.md) keeps ordinary destinations unchanged and preserves xAI custom-call ID repair. `src/config/schema/compaction-recovery.ts` strictly validates opt-in `compactionRecovery`; invalid disk values disable it with a warning, while candidate writes reject them. `src/config/schema/blocked-model-redirects.ts` applies the same read-degrade/write-reject boundary to malformed `blockedModelRedirects` maps. The [failure-only contract](transports/responses-failover.md) leaves provider identity, accounts and client compaction unchanged. `src/cli/config-command.ts` accepts one leading UTF-8 BOM when parsing validate/import input from a file or stdin. JSON syntax and schema validation still run before persistence; BOM characters inside string values remain data.
+Custom Responses providers can explicitly enable `preserveResponsesInputItemIds` and `preserveResponsesMessageMetadata`; both default off and accept only booleans. The [replay sanitization contract](transports/responses-wire-shapes.md) keeps ordinary destinations unchanged and preserves xAI custom-call ID repair. `src/config/schema/compaction-recovery.ts` strictly validates opt-in `compactionRecovery`; invalid disk values disable it with a warning, while candidate writes reject them. `src/config/schema/blocked-model-redirects.ts` applies the same read-degrade/write-reject boundary to malformed `blockedModelRedirects` maps. The [failure-only contract](transports/responses-failover.md) leaves provider identity, accounts and client compaction unchanged. Optional `desktopCompatibility: { startOnProxyStart: boolean }` likewise rejects invalid writes and degrades invalid disk values to off; it saves neither certificate consent nor correction mode. Its field-scoped dashboard writer adopts committed state into the live comparison baseline; see [Codex Desktop startup](clients/codex-desktop.md#proxy-startup-preference). `src/cli/config-command.ts` accepts one leading UTF-8 BOM when parsing validate/import input from a file or stdin. JSON syntax and schema validation still run before persistence; BOM characters inside string values remain data.
`skills.catalog_refresh` in the proxy JSON configuration accepts `per_session` (the runtime default when absent) or `per_turn`. The former retains received skills instructions for a conversation; the latter passes through the current catalog. This is separate from Codex's `skills.include_instructions` TOML switch and does not change the live dashboard probe. See the [Responses snapshot contract](transports/responses.md#responses-httpsse).
diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md
index 9ae2019fe9b..bddf4344805 100644
--- a/structure/gui-and-management-api.md
+++ b/structure/gui-and-management-api.md
@@ -1,5 +1,13 @@
# GUI And Management API
+The optional Windows compatibility runtime uses confirmed local GUI-session commands; raw admin-token and remote ingress mutations are
+refused. Its start/stop/launch and three-minute account-UI trial follow the [native compatibility contract](clients/codex-desktop.md#optional-compatibility-runtime).
+The lazy Codex Set desktop tab uses the machine API target and follows the
+[dashboard consent and stale-response contract](clients/codex-desktop.md#dashboard-controls).
+Runtime management and proxy startup share a single owner and register awaited shutdown; the desktop compatibility
+settings endpoint binds each local GUI write to the displayed field revision. Saving the next-start preference
+triggers no runtime action.
+
Anthropic OAuth account DTOs include `autoSwitchThresholdOverride` (integer or null),
`autoSwitchThreshold` (pool default) and `effectiveAutoSwitchThreshold`. The dedicated
`PUT /api/oauth/accounts/auto-switch` accepts `{ provider: "anthropic", accountId, threshold }`;
@@ -96,6 +104,8 @@ unsupported; deployments that previously relied on such embedding must open it a
## Authentication boundaries
+Codex compatibility certificate setup follows the [certificate setup API](clients/codex-desktop.md#certificate-setup-api): status is read-only; key/trust mutations require the actual local GUI-session principal, explicit confirmation and an exact fingerprint for trust changes. The endpoint does not enable a relay, change Codex login or restart the app.
+
Kiro management login starts the native device flow only when `POST /api/oauth/login`
supplies `method: "builder-id"`, `"google"`, or `"github"`. A method-less request retains
the Kiro CLI flow used by the dashboard chooser. The KiroDeviceLoginDialog and useKiroDeviceLogin GUI modules own the native chooser and polling. The kiro-device-login-finalizer GUI module continues terminal status reads after dialog unmount; a provisional cancel result is never treated as confirmed success. Hook and finalizer share one status-read operation that owns fetch, bounded body consumption, parsing and cancellation. The complete read has a 45-second ceiling, the detached loop remains bounded by flow expiry plus 60 seconds (and 16 minutes maximum), and closing the dialog transfers rather than clones an in-flight reader so an already-observed terminal reply can still win. Native status and cancellation require `flowId`;
diff --git a/structure/manifest.json b/structure/manifest.json
index 48d655727f8..0f8000dcfa6 100644
--- a/structure/manifest.json
+++ b/structure/manifest.json
@@ -459,6 +459,13 @@
"src/lib/"
]
},
+ {
+ "path": "clients/codex-desktop.md",
+ "tier": 5,
+ "title": "Codex Desktop Compatibility",
+ "scope": "Windows package activation, restart context, and protected compatibility authority persistence.",
+ "documents": ["src/codex/", "src/cli/"]
+ },
{
"path": "clients/chatgpt-desktop.md",
"tier": 5,
diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md
index ff43997aa44..1435778c235 100644
--- a/structure/ops/docs-and-release.md
+++ b/structure/ops/docs-and-release.md
@@ -422,9 +422,9 @@ The `package-standalone` job in `.github/workflows/release.yml` also builds Bun
`ocx` archives for Linux, macOS, and Windows, bundles `gui/dist`, smoke-tests `/healthz`, and
publishes SHA-256 sidecars for the attach job. Each archive also carries the target-matching
`@napi-rs/keyring` native addon under `keyring/`; the macOS release installs both optional Darwin
-packages so its separate arm64 and x64 builds cannot silently reuse the hosted runner's
-architecture. Desktop preparation copies those same pinned assets into Tauri resources. The loader
-and packaged-app proof are owned by the [desktop keyring contract](../desktop-shell.md#packaged-native-keyring-binding).
+packages so its separate arm64 and x64 builds cannot silently reuse the hosted runner's architecture. Desktop preparation
+copies those same pinned assets into Tauri resources. The loader and packaged-app proof are owned by the [desktop keyring contract](../desktop-shell.md#packaged-native-keyring-binding).
+`src/lib/standalone.ts` recognizes Bun's file-URL virtual roots after one URL decode, including Windows `%7EBUN`, and resolves runtime assets beside the real executable. Invalid/non-file URLs and double-encoded markers remain source paths; `tests/lib/standalone.test.ts` and `tests/service/standalone-service.test.ts` cover this distinction.
Opening a release starts with the `dev` pre-move. Dispatch
`.github/workflows/dev-version-bump.yml` with the intended version, merge the pull request it opens,
diff --git a/structure/runtime.md b/structure/runtime.md
index eaaef86bb72..df56e9f61d3 100644
--- a/structure/runtime.md
+++ b/structure/runtime.md
@@ -1,6 +1,6 @@
# Runtime
-The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract).
+The minute sweep checks persisted activation deadlines locally; only missing deadlines trigger metadata discovery. See the [quota activation contract](providers/openai-tiers.md#public-provider-contract). Optional native desktop observation follows the [Codex Desktop startup gate and awaited teardown](clients/codex-desktop.md#proxy-startup-preference).
## Resolved static model policy
@@ -67,7 +67,7 @@ Catalog-derived reasoning-level diagnostics are escaped only at the human-output
## CLI Codex restart scope
-`ocx system codex-restart` requests a full Codex desktop-app restart and app-server restarts through the management endpoint. `src/cli/capabilities.ts` names that scope and warns that unsaved composer drafts, model-picker selections, and pending approval prompts may be discarded. `src/cli/system-command.ts` repeats that concrete state-loss warning both when confirmation is missing and after a confirmed human-readable request; the unconfirmed path sends no restart request. `--json` preserves the complete server result, including skipped or refused desktop outcomes. An armed test process never reaches the real desktop app. When the test preload's `OCX_TEST_HOME_GUARD=1` is set and the caller injected no `execFile`, `restartCodexDesktopApp` in `src/codex/desktop-app-restart.ts` returns the skipped reason `test_environment` before discovery or signalling, and `handleDesktopAppRestart` in `src/cli/restart-scope.ts` reports that skip. The flag, not `NODE_ENV`, decides, so a real `NODE_ENV=test ocx ...` still restarts the app; adapter tests that inject `execFile` still exercise the full path. `tests/clients/desktop-app-restart.test.ts` covers the skip.
+The explicit restart, Windows package activation and compatibility authority contracts are documented in [Codex Desktop compatibility](clients/codex-desktop.md).
After a CLI catalog/cache write, advisory restart guidance compares each running Codex app-server's
start time with the written catalog mtime. It reports only processes proven stale; a fresh or
diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md
index f0de552888a..41a3eeeefbc 100644
--- a/structure/transports/inventory.md
+++ b/structure/transports/inventory.md
@@ -307,6 +307,10 @@ admission or account-snapshot pairing. The forwarding contract is covered in
## SOCKS5 dispatch boundary
+`src/lib/socks5-handshake.ts` shares RFC1928/RFC1929 framing between the fetch tunnel and
+native desktop raw TLS dial. Credential decoding is bounded and failures retain the fetch
+transport's public `Socks5FetchError` contract. This extraction reuses lcxhh521's PR5947 work.
+
`src/config/proxy-env.ts` activates configured SOCKS5 through `src/lib/proxy-env.ts`;
the compatibility config facade does not own a second activation path.
`src/server/responses/fetch-helpers.ts` routes the built-in HTTP executor through
@@ -379,3 +383,21 @@ Dashboard Fast-row persistence and client refresh follow the [Fast selector rows
The [compaction routing override](responses-failover.md#compaction-routing-overrides) selects a target before the existing native compact or routed Responses transport is resolved.
First-party managed native Messages retain the [serving UUID and observed CLI identity contract](../data-planes/protocol-paths.md#managed-native-messages). Identity headers do not authorize credentials or extend their destination scope.
+
+## Native desktop proxy egress
+
+`src/lib/desktop-proxy-route.ts` binds desktop HTTP fetch and raw upgraded sockets to the
+same explicit proxy decision. NO_PROXY may choose direct; otherwise HTTPS_PROXY or a
+selected SOCKS5 ALL_PROXY is carried explicitly. With no HTTPS_PROXY, the desktop relay also
+binds HTTP(S) ALL_PROXY explicitly to its supported CONNECT transport. An invalid HTTPS_PROXY
+never falls through to ALL_PROXY. Present but unsupported proxy configuration
+refuses. A failed selected route never retries directly. Identity verification uses this same
+HTTP path, while provider inference retains its separate existing provider-egress policy.
+
+`src/lib/desktop-upstream-tunnel.ts` dials fixed chatgpt.com TLS through HTTP/HTTPS CONNECT
+or authenticated SOCKS5. It bounds the whole connection setup, rejects proxy EOF, abort and
+oversized handshake heads, and verifies both proxy and upstream certificate identities.
+Only the proxy handshake carries proxy credentials. The raw upgraded socket remains opaque;
+the relay does not decode WebSocket payloads. After CONNECT the paused outer stream resumes
+before TLS-over-TLS negotiation. Test fixture CA trust is confined to child processes or
+injected test socket options, never the user's OS trust store.
diff --git a/tests/cli/cli-headless-parity.test.ts b/tests/cli/cli-headless-parity.test.ts
index c613ab3d3c9..a2560098049 100644
--- a/tests/cli/cli-headless-parity.test.ts
+++ b/tests/cli/cli-headless-parity.test.ts
@@ -572,6 +572,9 @@ describe("headless GUI parity CLI", () => {
// Claude reset grants: reading is an owed CLI verb (deferred-verb in the route
// registry) and spending is dashboard-session-only by design.
["/api/anthropic/reset-grants", "(none — GUI reset-grant dialog; spend requires a dashboard session)"],
+ // The registry records read-only status as an owed CLI verb; trust, launch,
+ // settings and the account-wide trial intentionally require local GUI consent.
+ ["/api/codex/desktop-compatibility", "(none — local GUI confirmation; status CLI deferred in clients/codex-desktop.md)"],
["/api/protocols", "ocx api protocols/explain/policy"],
["/api/settings", "ocx system"],
// Routing Intelligence (RI-04..RI-10): profiles + dry-run are mirrored by
@@ -1163,13 +1166,17 @@ describe("headless GUI parity CLI", () => {
test("remote connect status is headless and revoke refuses disconnected state before hub traffic", async () => {
let requests = 0;
+ const lockRoot = mkdtempSync(join(tmpdir(), "ocx-connect-parity-lock-"));
+ const lifecycleLockDeps = { lockPath: join(lockRoot, "client-lifecycle.sqlite") };
const logSpy = spyOn(console, "log").mockImplementation(() => {});
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
try {
expect(await handleConnectCommand(["status", "--json"], {
+ lifecycleLockDeps,
fetchImpl: async () => { requests += 1; return new Response(); },
})).toBe(0);
expect(await handleConnectCommand(["revoke", "--admin-token-stdin", "--json"], {
+ lifecycleLockDeps,
stdinImpl: Readable.from(["ocx_admin_test\n"]),
fetchImpl: async () => { requests += 1; return new Response(); },
})).toBe(1);
@@ -1177,6 +1184,7 @@ describe("headless GUI parity CLI", () => {
} finally {
logSpy.mockRestore();
errorSpy.mockRestore();
+ removeTreeWithRetry(lockRoot);
}
});
diff --git a/tests/clients/desktop-app-restart.test.ts b/tests/clients/desktop-app-restart.test.ts
index 1fd3de3c26e..8bbfcec3503 100644
--- a/tests/clients/desktop-app-restart.test.ts
+++ b/tests/clients/desktop-app-restart.test.ts
@@ -171,6 +171,28 @@ describe("the test-runner guard follows the test preload, not NODE_ENV", () => {
});
describe("Codex desktop app restart (#2292)", () => {
+ test("an unreadable Windows root command line refuses before stop and leaves the restart lock reusable", () => {
+ const calls: Call[] = [];
+ const io = scriptedIo({ discovery: DISCOVERY, processes: `1000 900 2026-01-01T00:00:00Z ${INSTALL}\\app\\ChatGPT.exe\t`, calls });
+ withTrustedExes(() => {
+ for (let attempt = 0; attempt < 2; attempt++) {
+ expect(restartCodexDesktopApp(io)).toEqual({ attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason: "relaunch_context_failed" });
+ }
+ });
+ expect(calls.some(call => call.file === TASKKILL || /CloseMainWindow|Start-Process|OpenCodexPackageActivation/.test(call.args.join(" ")))).toBe(false);
+ });
+
+ test("a relaunch-context failure refuses before any process is signalled and releases the lock", () => {
+ const calls: Call[] = [];
+ const io = scriptedIo({ discovery: DISCOVERY, processes: `1000 900 2026-01-01T00:00:00Z ${INSTALL}\\app\\ChatGPT.exe`, calls });
+ io.adapter = { ...windowsDesktopAppAdapter, captureRelaunchContext: () => { throw new Error("conflicting context"); } };
+ withTrustedExes(() => {
+ const first = restartCodexDesktopApp(io), second = restartCodexDesktopApp(io);
+ expect(first).toEqual({ attempted: false, stopped: [], surviving: [], relaunch: "skipped", reason: "relaunch_context_failed" });
+ expect(second.reason).toBe("relaunch_context_failed");
+ });
+ expect(calls.some(call => call.file === TASKKILL || call.args.join(" ").includes("CloseMainWindow") || call.args.join(" ").includes("Start-Process"))).toBe(false);
+ });
// macOS and Linux are no longer no-ops: they have real adapters. What survives from the
// original assertion is that a platform with NO adapter still refuses without execing
// anything, which is the fail-closed property the old windows_only case was really
@@ -504,4 +526,3 @@ describe("#2557 a failed probe is not an absent app", () => {
expect(script).not.toContain("SilentlyContinue' $root");
});
});
-
diff --git a/tests/clients/desktop-compatibility-authority.test.ts b/tests/clients/desktop-compatibility-authority.test.ts
new file mode 100644
index 00000000000..f23e2ae70a0
--- /dev/null
+++ b/tests/clients/desktop-compatibility-authority.test.ts
@@ -0,0 +1,174 @@
+import { afterAll, describe, expect, test } from "bun:test";
+import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, X509Certificate } from "node:crypto";
+import { createCertificateAuthority } from "../../src/claude/intercept/local-ca";
+import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority } from "../../src/codex/desktop-compatibility/certificate-store";
+import { windowsAuthorityKeyProtection, type AuthorityKeyProtection } from "../../src/codex/desktop-compatibility/windows-key-protection";
+import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl";
+
+const roots: string[] = [];
+const success = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" });
+// These temporary-store cases exercise publication semantics, not the separate ACL implementation.
+setIcaclsRunnerForTests(success);
+setAsyncIcaclsRunnerForTests(async () => success());
+afterAll(() => {
+ setIcaclsRunnerForTests(null); setAsyncIcaclsRunnerForTests(null);
+ for (const root of roots) rmSync(root, { recursive: true });
+});
+const directory = () => { const root = mkdtempSync(join(tmpdir(), "ocx-compat-authority-")); roots.push(root); return root; };
+function protector(): AuthorityKeyProtection {
+ const key = randomBytes(32);
+ return {
+ async protect(cleartext) {
+ const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv);
+ const encrypted = Buffer.concat([cipher.update(cleartext), cipher.final()]);
+ return Buffer.concat([iv, cipher.getAuthTag(), encrypted]);
+ },
+ async unprotect(ciphertext) {
+ const data = Buffer.from(ciphertext), cipher = createDecipheriv("aes-256-gcm", key, data.subarray(0, 12));
+ cipher.setAuthTag(data.subarray(12, 28));
+ return Buffer.concat([cipher.update(data.subarray(28)), cipher.final()]);
+ },
+ };
+}
+
+describe("Codex Desktop compatibility authority lifecycle", () => {
+ test("legacy-purpose authority stays removable but must be deliberately renewed before reuse", async () => {
+ const root = directory(), protection = protector(), commonName = `OpenCodex Codex Desktop ${randomUUID()}`;
+ const legacy = createCertificateAuthority({ commonName, validityDays: 30, permittedDnsNames: ["chatgpt.com"] });
+ expect(new X509Certificate(legacy.certPem).keyUsage).toBeUndefined();
+ const clear = Buffer.from(JSON.stringify({ policy: "codex-desktop-chatgpt-only/v1", commonName,
+ certSha256: createHash("sha256").update(legacy.certPem).digest("hex"), keyPem: legacy.keyPem }));
+ const path = join(root, "authority.json");
+ writeFileSync(path, JSON.stringify({ version: 1, protection: "windows-current-user-dpapi", certPem: legacy.certPem,
+ sealed: Buffer.from(await protection.protect(clear)).toString("base64") })); clear.fill(0);
+ const before = readFileSync(path, "utf8");
+ expect(inspectDesktopCompatibilityAuthority(root).status).toBe("renewal-required");
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection })).rejects.toMatchObject({ code: "renewal_required" });
+ await expect(loadDesktopCompatibilityAuthority({ directory: root, protection })).rejects.toMatchObject({ code: "renewal_required" });
+ const removable = await loadDesktopCompatibilityAuthority({ directory: root, protection }, true);
+ expect(readFileSync(path, "utf8")).toBe(before);
+ const renewed = await renewDesktopCompatibilityAuthority({ directory: root, protection }, removable.fingerprint);
+ expect(renewed.fingerprint).not.toBe(removable.fingerprint);
+ expect(new X509Certificate(renewed.authority.certPem).keyUsage).toEqual(["1.3.6.1.5.5.7.3.1"]);
+ expect(inspectDesktopCompatibilityAuthority(root).status).toBe("present");
+ });
+
+ test("deliberate renewal replaces one encrypted identity and refuses a stale fingerprint", async () => {
+ const root = directory(), protection = protector();
+ const first = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const second = await renewDesktopCompatibilityAuthority({ directory: root, protection }, first.fingerprint);
+ expect(second.fingerprint).not.toBe(first.fingerprint);
+ expect(second.reused).toBe(false);
+ expect((await ensureDesktopCompatibilityAuthority({ directory: root, protection })).fingerprint).toBe(second.fingerprint);
+ const contents = readFileSync(join(root, "authority.json"), "utf8");
+ await expect(renewDesktopCompatibilityAuthority({ directory: root, protection }, first.fingerprint)).rejects.toMatchObject({ code: "fingerprint_changed" });
+ expect(readFileSync(join(root, "authority.json"), "utf8")).toBe(contents);
+ expect(readdirSync(root).filter(name => /authority.*\.(json|tmp)$/.test(name))).toEqual(["authority.json"]);
+ });
+
+ test("failed renewal preserves the old removable envelope and cleans unpublished staging", async () => {
+ const root = directory(), protection = protector();
+ const first = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const original = readFileSync(join(root, "authority.json"), "utf8");
+ const broken = { ...protection, protect: async () => Buffer.from("unreadable replacement") };
+ await expect(renewDesktopCompatibilityAuthority({ directory: root, protection: broken }, first.fingerprint)).rejects.toMatchObject({ code: "unreadable" });
+ expect(readFileSync(join(root, "authority.json"), "utf8")).toBe(original);
+ expect((await loadDesktopCompatibilityAuthority({ directory: root, protection }, true)).fingerprint).toBe(first.fingerprint);
+ expect(readdirSync(root).some(name => name.endsWith(".tmp"))).toBe(false);
+ });
+
+ test("public status neither creates missing state nor repairs a malformed envelope", async () => {
+ const root = directory(), absent = join(root, "not-created");
+ expect(inspectDesktopCompatibilityAuthority(absent)).toEqual({ status: "missing" }); expect(existsSync(absent)).toBe(false);
+ const prepared = await ensureDesktopCompatibilityAuthority({ directory: root, protection: protector() });
+ const path = join(root, "authority.json"), original = readFileSync(path, "utf8");
+ expect(inspectDesktopCompatibilityAuthority(root)).toMatchObject({ status: "present", fingerprint: prepared.fingerprint });
+ expect(readFileSync(path, "utf8")).toBe(original);
+ const malformed = JSON.parse(original); delete malformed.sealed; writeFileSync(path, JSON.stringify(malformed));
+ const before = readFileSync(path, "utf8"); expect(inspectDesktopCompatibilityAuthority(root)).toEqual({ status: "invalid" });
+ expect(readFileSync(path, "utf8")).toBe(before);
+ });
+
+ test("an expired key can be loaded for exact trust removal without minting a replacement", async () => {
+ const root = directory(), protection = protector();
+ const prepared = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const options = { directory: root, protection, now: () => prepared.expiresAt + 1 };
+ await expect(loadDesktopCompatibilityAuthority(options)).rejects.toMatchObject({ code: "expired" });
+ const loaded = await loadDesktopCompatibilityAuthority(options, true);
+ expect(loaded.fingerprint).toBe(prepared.fingerprint); expect(loaded.authority.keyPem).toBe(prepared.authority.keyPem);
+ });
+
+ test("reuses the exact certificate and key after reopening; disk never contains the private PEM", async () => {
+ const root = directory(), protection = protector();
+ const first = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const contents = readFileSync(join(root, "authority.json"), "utf8");
+ expect(contents).not.toContain("PRIVATE KEY");
+ expect(contents).not.toContain(first.authority.keyPem);
+ const second = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ expect(first.reused).toBe(false); expect(second.reused).toBe(true);
+ expect(second.fingerprint).toBe(first.fingerprint);
+ expect(second.authority.keyPem).toBe(first.authority.keyPem);
+ expect(readFileSync(join(root, "authority.json"), "utf8")).toBe(contents);
+ });
+
+ test("a foreign user or corrupt protected key refuses without replacing the trusted identity", async () => {
+ const root = directory(), protection = protector();
+ await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const path = join(root, "authority.json"), original = readFileSync(path, "utf8");
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection: protector() }))
+ .rejects.toMatchObject({ code: "unreadable" });
+ expect(readFileSync(path, "utf8")).toBe(original);
+ const saved = JSON.parse(original); saved.sealed = "invalid-ciphertext"; writeFileSync(path, JSON.stringify(saved));
+ const corrupted = readFileSync(path, "utf8");
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection })).rejects.toMatchObject({ code: "unreadable" });
+ expect(readFileSync(path, "utf8")).toBe(corrupted);
+ });
+
+ test("swapping only the public certificate cannot reuse an unrelated protected key", async () => {
+ const protection = protector(), root = directory(), foreign = directory();
+ await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ await ensureDesktopCompatibilityAuthority({ directory: foreign, protection });
+ const path = join(root, "authority.json"), saved = JSON.parse(readFileSync(path, "utf8"));
+ saved.certPem = JSON.parse(readFileSync(join(foreign, "authority.json"), "utf8")).certPem;
+ writeFileSync(path, JSON.stringify(saved));
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection })).rejects.toMatchObject({ code: "unreadable" });
+ });
+
+ test("expiry requires deliberate renewal and never silently creates a new trust prompt", async () => {
+ const root = directory(), protection = protector();
+ const first = await ensureDesktopCompatibilityAuthority({ directory: root, protection });
+ const original = readFileSync(join(root, "authority.json"), "utf8");
+ const near = await ensureDesktopCompatibilityAuthority({ directory: root, protection, now: () => first.expiresAt - 60_000 });
+ expect(near.renewalDue).toBe(true); expect(near.fingerprint).toBe(first.fingerprint);
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection, now: () => first.expiresAt + 1 }))
+ .rejects.toMatchObject({ code: "expired" });
+ expect(readFileSync(join(root, "authority.json"), "utf8")).toBe(original);
+ });
+
+ test("protection failure publishes no certificate or plaintext fallback", async () => {
+ const root = directory();
+ const protection: AuthorityKeyProtection = { protect: async () => { throw new Error("fixture secret"); }, unprotect: async () => { throw new Error(); } };
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection })).rejects.toMatchObject({ code: "protection_failed" });
+ expect(existsSync(join(root, "authority.json"))).toBe(false);
+ expect(readdirSync(root).some(name => name.endsWith(".tmp") || name.endsWith(".pem") || name.endsWith(".key"))).toBe(false);
+ });
+
+ test("malformed existing state is preserved for recovery rather than regenerated", async () => {
+ const root = directory(), path = join(root, "authority.json");
+ writeFileSync(path, "broken state");
+ await expect(ensureDesktopCompatibilityAuthority({ directory: root, protection: protector() })).rejects.toMatchObject({ code: "unreadable" });
+ expect(readFileSync(path, "utf8")).toBe("broken state");
+ });
+
+ test.skipIf(process.platform !== "win32")("real CurrentUser DPAPI survives separate helper invocations and rejects corruption", async () => {
+ const original = Buffer.from("synthetic authority secret; no real credentials");
+ const encrypted = await windowsAuthorityKeyProtection.protect(original);
+ expect(Buffer.from(encrypted).includes(original)).toBe(false);
+ expect(Buffer.from(await windowsAuthorityKeyProtection.unprotect(encrypted))).toEqual(original);
+ const corrupted = Uint8Array.from(encrypted); corrupted[corrupted.length - 1] ^= 1;
+ await expect(windowsAuthorityKeyProtection.unprotect(corrupted)).rejects.toThrow("desktop_compatibility_key_protection_failed");
+ }, 35_000);
+});
diff --git a/tests/clients/desktop-compatibility-build-probe.test.ts b/tests/clients/desktop-compatibility-build-probe.test.ts
new file mode 100644
index 00000000000..a412e914592
--- /dev/null
+++ b/tests/clients/desktop-compatibility-build-probe.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from "bun:test";
+import { createInstalledBuildProbe, DESKTOP_COMPATIBILITY_ASSESSED_VERSION, DESKTOP_COMPATIBILITY_ASSESSED_FAMILY } from "../../src/codex/desktop-compatibility/installed-build";
+import type { DesktopAppInstall } from "../../src/codex/desktop-app/types";
+
+const installed = (version = DESKTOP_COMPATIBILITY_ASSESSED_VERSION): DesktopAppInstall => ({
+ id: DESKTOP_COMPATIBILITY_ASSESSED_FAMILY, root: `C:\\Program Files\\WindowsApps\\OpenAI.Codex_${version}_x64__2p2nqsd0c76g0`, relaunch: `${DESKTOP_COMPATIBILITY_ASSESSED_FAMILY}!App`,
+});
+
+test("matching version text cannot qualify a foreign package family, publisher or app entry", async () => {
+ const original = installed();
+ for (const candidate of [{ ...original, id: "Foreign.Codex_2p2nqsd0c76g0" },
+ { ...original, relaunch: `${original.id}!Other` },
+ { ...original, root: original.root.replace("__2p2nqsd0c76g0", "__foreign") },
+ { ...original, id: "OpenAI.Codex_foreign", relaunch: "OpenAI.Codex_foreign!App", root: original.root.replace("__2p2nqsd0c76g0", "__foreign") },
+ { ...original, root: original.root + "_extra" }]) {
+ const probe = createInstalledBuildProbe(async () => candidate);
+ expect(await probe.check()).toBe(false); await probe.close();
+ }
+});
+
+test("concurrent build checks share only the pending query and refresh after it settles", async () => {
+ let calls = 0, release!: (value: DesktopAppInstall | null) => void;
+ const probe = createInstalledBuildProbe(async () => { calls++; return new Promise(resolve => { release = resolve; }); });
+ const first = probe.check(), second = probe.check();
+ await Bun.sleep(0); expect(calls).toBe(1);
+ release(installed()); expect(await first).toBe(true); expect(await second).toBe(true);
+ const changed = probe.check(); await Bun.sleep(0); expect(calls).toBe(2);
+ release(installed("99.1.1.0")); expect(await changed).toBe(false);
+ await probe.close();
+});
+
+test("shutdown aborts the owned query and waits for its cleanup before settling", async () => {
+ let aborted = false, reaped!: () => void, closed = false;
+ const probe = createInstalledBuildProbe(signal => new Promise(resolve => {
+ reaped = () => resolve(null);
+ signal.addEventListener("abort", () => { aborted = true; }, { once: true });
+ }));
+ const pending = probe.check(); await Bun.sleep(0);
+ const closing = probe.close().then(() => { closed = true; });
+ await Bun.sleep(0); expect(aborted).toBe(true); expect(closed).toBe(false);
+ reaped(); await closing; expect(await pending).toBe(false); expect(await probe.check()).toBe(false);
+});
+
+test("a failed probe is closed and the next fresh query can recover", async () => {
+ let calls = 0;
+ const probe = createInstalledBuildProbe(async () => { if (++calls === 1) throw new Error("fixture failure"); return installed(); });
+ expect(await probe.check()).toBe(false); expect(await probe.check()).toBe(true);
+ await probe.close();
+});
diff --git a/tests/clients/desktop-compatibility-certificate-service.test.ts b/tests/clients/desktop-compatibility-certificate-service.test.ts
new file mode 100644
index 00000000000..638e2a3d30a
--- /dev/null
+++ b/tests/clients/desktop-compatibility-certificate-service.test.ts
@@ -0,0 +1,88 @@
+import { describe, expect, test } from "bun:test";
+import { createDesktopCertificateService } from "../../src/codex/desktop-compatibility/certificate-service";
+import type { DesktopAuthorityInspection, StoredDesktopAuthority } from "../../src/codex/desktop-compatibility/certificate-store";
+
+const fingerprint = "A".repeat(64);
+const authority = { fingerprint } as StoredDesktopAuthority;
+function fixture() {
+ const calls: string[] = [];
+ let stored: DesktopAuthorityInspection = { status: "missing" }, trusted = false;
+ let appRunning: boolean | null = false, removalResult: "not-trusted" | "unknown" | "trusted" = "not-trusted";
+ const present = () => { stored = { status: "present", fingerprint, certPem: "public-fixture", expiresAt: 123456, renewalDue: false }; };
+ const service = createDesktopCertificateService("fixture-only", { platform: "win32",
+ inspect: () => { calls.push("inspect"); return stored; },
+ prepare: async () => { calls.push("prepare"); present(); return authority; },
+ load: async expired => { calls.push(expired ? "load-removal" : "load"); return authority; },
+ renew: async () => { calls.push("renew"); stored = { status: "present", fingerprint: "B".repeat(64), certPem: "renewed-public-fixture", expiresAt: 234567, renewalDue: false }; return { fingerprint: "B".repeat(64) } as StoredDesktopAuthority; },
+ readTrust: async () => { calls.push("read-trust"); return trusted ? "trusted" : "not-trusted"; },
+ changeTrust: async (_value, action) => { calls.push(action); trusted = action === "trust"; return trusted ? "trusted" : removalResult; },
+ appRunning: async () => appRunning,
+ });
+ return { calls, service, present, setAppRunning: (value: boolean | null) => { appRunning = value; }, setRemovalResult: (value: typeof removalResult) => { removalResult = value; }, setStored: (value: DesktopAuthorityInspection) => { stored = value; } };
+}
+
+describe("compatibility certificate setup service", () => {
+ test("legacy-purpose status stays renewal-required even when its old OS root is trusted", async () => {
+ const io = fixture(); io.present(); await io.service.trust(fingerprint);
+ io.setStored({ status: "renewal-required", fingerprint, certPem: "public-fixture", expiresAt: 123456, renewalDue: false });
+ expect((await io.service.status()).state).toBe("renewal-required");
+ expect((await io.service.renew(fingerprint)).state).toBe("prepared");
+ expect(io.calls).toContain("load-removal");
+ });
+
+ test("renewal verifies removal before replacement and leaves the new root untrusted", async () => {
+ const io = fixture(); io.present();
+ const result = await io.service.renew(fingerprint);
+ expect(result).toMatchObject({ state: "prepared", fingerprint: "B".repeat(64) });
+ expect(io.calls.indexOf("remove")).toBeLessThan(io.calls.indexOf("renew"));
+ expect(io.calls).toContain("load-removal"); expect(io.calls).not.toContain("trust");
+ expect(io.calls).not.toContain("prepare");
+ });
+ test("renewal never loses an old trust identity after refusal, uncertainty or a running app", async () => {
+ for (const result of ["unknown", "trusted"] as const) {
+ const io = fixture(); io.present(); io.setRemovalResult(result);
+ await expect(io.service.renew(fingerprint)).rejects.toMatchObject({ code: result === "unknown" ? "trust_unknown" : "trust_not_applied" });
+ expect(io.calls).not.toContain("renew");
+ }
+ const io = fixture(); io.present(); io.setAppRunning(true);
+ await expect(io.service.renew(fingerprint)).rejects.toMatchObject({ code: "app_running" });
+ expect(io.calls).not.toContain("remove"); expect(io.calls).not.toContain("renew");
+ });
+
+ test("status and unsupported hosts never create a key or inspect trust unnecessarily", async () => {
+ const io = fixture(); expect((await io.service.status()).state).toBe("missing"); expect(io.calls).toEqual(["inspect"]);
+ const disabled = createDesktopCertificateService("unused", { platform: "linux", inspect: () => { throw new Error("must not run"); } });
+ expect(await disabled.status()).toEqual({ supported: false, state: "missing", busy: null });
+ await expect(disabled.prepare()).rejects.toMatchObject({ code: "unsupported" });
+ });
+ test("prepare does not register trust and trust loads only an already prepared key", async () => {
+ const io = fixture();
+ expect((await io.service.prepare()).state).toBe("prepared"); expect(io.calls).not.toContain("trust");
+ io.calls.length = 0;
+ expect((await io.service.trust(fingerprint)).state).toBe("trusted");
+ expect(io.calls).toContain("load"); expect(io.calls).not.toContain("prepare");
+ });
+ test("missing or changed certificates cannot silently generate replacements during trust", async () => {
+ const io = fixture();
+ await expect(io.service.trust(fingerprint)).rejects.toMatchObject({ code: "not_prepared" });
+ io.present(); await expect(io.service.trust("B".repeat(64))).rejects.toMatchObject({ code: "fingerprint_changed" });
+ expect(io.calls).not.toContain("prepare"); expect(io.calls).not.toContain("trust");
+ });
+ test("removal waits until the app is absent and uses the removal-only existing-key load", async () => {
+ const io = fixture(); io.present(); io.setAppRunning(true);
+ await expect(io.service.removeTrust(fingerprint)).rejects.toMatchObject({ code: "app_running" });
+ expect(io.calls).not.toContain("remove"); io.setAppRunning(false);
+ expect((await io.service.removeTrust(fingerprint)).state).toBe("prepared");
+ expect(io.calls).toContain("load-removal"); expect(io.calls).not.toContain("prepare");
+ });
+ test("the public status never includes the certificate body or private-key object", async () => {
+ const io = fixture(); io.present();
+ const json = JSON.stringify(await io.service.status());
+ expect(json).not.toContain("certPem"); expect(json).not.toContain("public-fixture"); expect(json).not.toContain("authority");
+ });
+ test("unknown app state is reported distinctly and cannot remove trust", async () => {
+ const io = fixture(); io.present(); io.setAppRunning(null);
+ await expect(io.service.removeTrust(fingerprint)).rejects.toMatchObject({ code: "app_state_unknown" });
+ expect(io.calls).not.toContain("remove"); expect(io.calls).not.toContain("load-removal");
+ });
+});
diff --git a/tests/clients/desktop-compatibility-connection-store.test.ts b/tests/clients/desktop-compatibility-connection-store.test.ts
new file mode 100644
index 00000000000..c9bc172cef3
--- /dev/null
+++ b/tests/clients/desktop-compatibility-connection-store.test.ts
@@ -0,0 +1,88 @@
+import { afterAll, expect, spyOn, test } from "bun:test";
+import * as fs from "node:fs";
+import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { randomUUID } from "node:crypto";
+import { X509Certificate } from "node:crypto";
+import { createDesktopConnectionStore } from "../../src/codex/desktop-compatibility/connection-store";
+import { createDesktopCompatibilityRuntime } from "../../src/codex/desktop-compatibility/runtime";
+import { createCertificateAuthority } from "../../src/claude/intercept/local-ca";
+import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl";
+const roots: string[] = [];
+const success = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" });
+setIcaclsRunnerForTests(success); setAsyncIcaclsRunnerForTests(async () => success());
+afterAll(() => { setIcaclsRunnerForTests(null); setAsyncIcaclsRunnerForTests(null); for (const root of roots) rmSync(root, { recursive: true }); });
+function fixture() { const path = mkdtempSync(join(tmpdir(), "ocx-desktop-endpoint-")); roots.push(path); return { path, store: createDesktopConnectionStore(path) }; }
+const identity = () => ({ version: 1 as const, id: randomUUID(), connectPort: 40001, pacPort: 40002 });
+
+test("failed temporary cleanup preserves the publication error as well as the residue", async () => {
+ const { path, store } = fixture(), primary = new Error("fixture publication failure"), cleanup = new Error("fixture cleanup failure");
+ const realLink = fs.linkSync, realUnlink = fs.unlinkSync;
+ const link = spyOn(fs, "linkSync").mockImplementation((source, destination) => {
+ if (String(destination) === join(path, "connection.json")) throw primary;
+ return realLink(source, destination);
+ });
+ const unlink = spyOn(fs, "unlinkSync").mockImplementation(target => {
+ if (String(target).startsWith(join(path, "connection-")) && String(target).endsWith(".tmp")) throw cleanup;
+ return realUnlink(target);
+ });
+ try {
+ const error = await store.publish(identity()).catch(value => value);
+ expect(error).toBeInstanceOf(Error); expect(error.message).toBe("desktop_compatibility_connection_cleanup_required");
+ expect(error.cause).toBeInstanceOf(AggregateError); expect(error.cause.errors).toEqual([primary, cleanup]);
+ expect(store.read()).toBeNull(); expect(readdirSync(path).some(value => value.endsWith(".tmp"))).toBe(true);
+ } finally { link.mockRestore(); unlink.mockRestore(); }
+});
+
+test("one public endpoint identity survives reopening without rewriting the file", async () => {
+ const { path, store } = fixture(), value = identity(); expect(store.read()).toBeNull();
+ await store.publish(value);
+ const saved = readFileSync(join(path, "connection.json"), "utf8");
+ const reopened = createDesktopConnectionStore(path);
+ expect(reopened.read()).toEqual(value); expect(await reopened.publish(value)).toEqual(value);
+ expect(readFileSync(join(path, "connection.json"), "utf8")).toBe(saved);
+ expect(Object.keys(JSON.parse(saved)).sort()).toEqual(["connectPort", "id", "pacPort", "version"]);
+ expect(readdirSync(path).some(file => file.endsWith(".tmp"))).toBe(false);
+});
+test("changed, invalid or unsupported state is preserved rather than assigned new endpoints", async () => {
+ const { path, store } = fixture(), value = identity(); await store.publish(value);
+ await expect(store.publish({ ...value, id: randomUUID() })).rejects.toThrow("connection_changed");
+ for (const content of ["broken", JSON.stringify({ ...value, connectPort: 80 }), JSON.stringify({ ...value, version: 2 }), JSON.stringify({ ...value, token: "fixture" })]) {
+ writeFileSync(join(path, "connection.json"), content);
+ expect(() => store.read()).toThrow("connection_invalid");
+ await expect(store.publish(value)).rejects.toThrow("connection_invalid");
+ expect(readFileSync(join(path, "connection.json"), "utf8")).toBe(content);
+ }
+});
+test("a competing publication cannot replace the first endpoint identity", async () => {
+ const { path, store } = fixture(), first = identity(), second = { ...identity(), connectPort: 40003, pacPort: 40004 };
+ const results = await Promise.allSettled([store.publish(first), createDesktopConnectionStore(path).publish(second)]);
+ expect(results.filter(value => value.status === "fulfilled")).toHaveLength(1);
+ expect([first.id, second.id]).toContain(store.read()!.id);
+ expect(readdirSync(path).some(file => file.endsWith(".tmp"))).toBe(false);
+});
+
+test("a fresh runtime reopens the persisted endpoints and serves an already cached PAC", async () => {
+ const { path } = fixture();
+ const authority = createCertificateAuthority({ commonName: "persisted-runtime-fixture", validityDays: 1 }), cert = new X509Certificate(authority.certPem);
+ const account = { id: "fixture", userId: "fixture-user", plan: "pro" as const, structure: "personal" as const };
+ const makeRuntime = () => createDesktopCompatibilityRuntime({ platform: "win32", testOnly: true,
+ connectionStore: createDesktopConnectionStore(path), identity: { readCurrentIdentity: async () => account, verifyFreshIdentity: async () => account },
+ loadAuthority: async () => ({ authority, commonName: "persisted-runtime-fixture", fingerprint: cert.fingerprint256.replaceAll(":", ""), expiresAt: Date.parse(cert.validTo), reused: true, renewalDue: false }),
+ trust: async () => "trusted", buildSupported: () => true, routingSupported: () => true,
+ upstreamFetch: (async () => Response.json({ fixture: "original-response" })) as typeof fetch,
+ });
+ const first = makeRuntime(), second = makeRuntime();
+ try {
+ await first.start(); const url = first.getPacUrl()!, pac = await fetch(url).then(res => res.text());
+ const saved = readFileSync(join(path, "connection.json"), "utf8");
+ const proxy = `http://127.0.0.1:${/PROXY 127\.0\.0\.1:(\d+)/.exec(pac)![1]}`;
+ const request = () => fetch("https://chatgpt.com/backend-api/conversation", { proxy, tls: { ca: authority.certPem } }).then(res => res.json());
+ expect(await request()).toEqual({ fixture: "original-response" });
+ await first.stop(); await second.start();
+ expect(second.getPacUrl()).toBe(url); expect(await fetch(url).then(res => res.text())).toBe(pac);
+ expect(await request()).toEqual({ fixture: "original-response" });
+ expect(readFileSync(join(path, "connection.json"), "utf8")).toBe(saved);
+ } finally { await first.stop(); await second.stop(); }
+}, 10000);
diff --git a/tests/clients/desktop-compatibility-launch.test.ts b/tests/clients/desktop-compatibility-launch.test.ts
new file mode 100644
index 00000000000..769b2971852
--- /dev/null
+++ b/tests/clients/desktop-compatibility-launch.test.ts
@@ -0,0 +1,161 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import { execFileSync } from "node:child_process";
+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";
+import type { DesktopExec } from "../../src/codex/desktop-app/types";
+import { windowsDesktopAppAdapter } from "../../src/codex/desktop-app/windows";
+import { activateWindowsCodexCompatibility, captureWindowsCompatibilityContext } from "../../src/codex/desktop-compatibility/windows-package-command";
+import { acquireDesktopCompatibilityRuntime, bindDesktopCompatibilityLaunch, readDesktopCompatibilityLaunch } from "../../src/codex/desktop-compatibility/runtime-ownership";
+
+const powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
+const install = { id: "OpenAI.Codex_fixture", root: "C:\\Program Files\\WindowsApps\\OpenAI.Codex_fixture", relaunch: "OpenAI.Codex_fixture!App" };
+const pac = "http://127.0.0.1:10102/fixture-0123456789/proxy.pac";
+const packageFullName = "OpenAI.Codex_1.2.3.0_x64__fixture";
+const releases: (() => void)[] = [];
+afterEach(() => { for (const release of releases.splice(0).reverse()) release(); setTrustedWindowsElevationExecutablesForTests(null); });
+function own(pacUrl = pac, current = () => true) {
+ const release = acquireDesktopCompatibilityRuntime(); releases.push(release);
+ releases.push(bindDesktopCompatibilityLaunch(pacUrl, current));
+ return { release, generation: readDesktopCompatibilityLaunch()!.generation };
+}
+function executor(options: { running?: string; probeFailure?: boolean; output?: string } = {}) {
+ const calls: string[] = [];
+ setTrustedWindowsElevationExecutablesForTests({ powershell });
+ const exec: DesktopExec = (_file, args) => {
+ const script = args.at(-1)!; calls.push(script);
+ if (script.includes("OpenCodexPackageActivation")) return options.output ?? JSON.stringify({ pid: 123, packageFullName, verified: true });
+ if (script.includes("Get-AppxPackage")) return `${install.id}\n${install.root}\n${install.relaunch}`;
+ if (options.probeFailure) throw new Error("probe refused");
+ return options.running ?? "";
+ };
+ return { calls, exec };
+}
+
+describe("Codex Desktop compatibility package launch", () => {
+ test("a same-shape PAC without a live runtime owner refuses before restart", () => {
+ const root = { pid: 100, parentPid: 50, createdAt: "fixture", executable: "ChatGPT.exe", commandLine: `ChatGPT.exe --proxy-pac-url=${pac}` };
+ expect(() => captureWindowsCompatibilityContext([root])).toThrow("desktop_compatibility_launch_owner_unverified");
+ });
+ test("activation tolerates a BOM and preceding warnings but normalizes invalid or timed-out output", () => {
+ const io = executor({ output: `\uFEFFwarning: fixture\r\n${JSON.stringify({ pid: 123, packageFullName, verified: true })}\r\n` });
+ expect(activateWindowsCodexCompatibility(io.exec, install, pac)).toEqual({ pid: 123, packageFullName });
+ for (const output of ["", "null", "[]", "warning only"]) {
+ expect(() => activateWindowsCodexCompatibility(executor({ output }).exec, install, pac)).toThrow("desktop_compatibility_activation_unverified");
+ }
+ expect(() => activateWindowsCodexCompatibility(() => { throw new Error("fixture timeout"); }, install, pac)).toThrow("desktop_compatibility_activation_unverified");
+ });
+ test("the shipped Windows restart adapter captures and reapplies an active compatibility PAC", () => {
+ const { generation } = own();
+ const commandLine = `"${install.root}\\app\\ChatGPT.exe" --proxy-pac-url=${pac}`;
+ const io = executor({ running: `100 50 2026-01-01T00:00:00Z ${install.root}\\app\\ChatGPT.exe\t${Buffer.from(commandLine).toString("base64")}` });
+ const processes = windowsDesktopAppAdapter.listProcesses(io.exec, install)!;
+ expect(processes[0]?.commandLine).toBe(commandLine);
+ const context = windowsDesktopAppAdapter.captureRelaunchContext(io.exec, install, processes);
+ expect(context).toEqual({ codexCompatibilityPacUrl: pac, codexCompatibilityGeneration: generation });
+ windowsDesktopAppAdapter.relaunch(io.exec, install, context);
+ expect(io.calls.at(-1)).toContain("OpenCodexPackageActivation");
+ expect(io.calls.at(-1)).toContain(pac);
+ });
+
+ test("an explicitly unreadable root command line survives parsing and refuses context capture", () => {
+ for (const commandLine of ["", " "]) {
+ const io = executor({ running: `100 50 2026-01-01T00:00:00Z ${install.root}\\app\\ChatGPT.exe\t${Buffer.from(commandLine).toString("base64")}` });
+ const processes = windowsDesktopAppAdapter.listProcesses(io.exec, install)!;
+ expect(processes).toHaveLength(1);
+ expect(processes[0]?.commandLine).toBe(commandLine);
+ expect(() => windowsDesktopAppAdapter.captureRelaunchContext(io.exec, install, processes))
+ .toThrow("desktop_compatibility_launch_context_unavailable");
+ }
+ });
+
+ test("helper arguments cannot override the main app and conflicting roots refuse before a stop", () => {
+ const { generation } = own();
+ const root = { pid: 100, parentPid: 50, createdAt: "fixture", executable: "ChatGPT.exe", commandLine: `ChatGPT.exe --proxy-pac-url=${pac}` };
+ const other = pac.replace(":10102", ":10103");
+ expect(captureWindowsCompatibilityContext([root, { ...root, pid: 101, parentPid: 100, commandLine: `ChatGPT.exe --proxy-pac-url=${other}` }]))
+ .toEqual({ codexCompatibilityPacUrl: pac, codexCompatibilityGeneration: generation });
+ expect(() => captureWindowsCompatibilityContext([root, { ...root, pid: 200, commandLine: `ChatGPT.exe --proxy-pac-url=${other}` }]))
+ .toThrow("desktop_compatibility_conflicting_launch_context");
+ expect(captureWindowsCompatibilityContext([{ ...root, commandLine: "ChatGPT.exe" }])).toEqual({});
+ expect(captureWindowsCompatibilityContext([root, { ...root, pid: 101, parentPid: 100, commandLine: "" }]))
+ .toEqual({ codexCompatibilityPacUrl: pac, codexCompatibilityGeneration: generation });
+ });
+
+ test("a foreign same-shape endpoint and an invalidated owner refuse capture", () => {
+ let current = true;
+ own(pac, () => current);
+ const root = { pid: 100, parentPid: 50, createdAt: "fixture", executable: "ChatGPT.exe", commandLine: `ChatGPT.exe --proxy-pac-url=${pac.replace(":10102", ":10103")}` };
+ expect(() => captureWindowsCompatibilityContext([root])).toThrow("desktop_compatibility_launch_owner_unverified");
+ current = false;
+ expect(() => captureWindowsCompatibilityContext([{ ...root, commandLine: `ChatGPT.exe --proxy-pac-url=${pac}` }]))
+ .toThrow("desktop_compatibility_launch_owner_unverified");
+ });
+
+ test("a stopped or replaced runtime cannot reuse a previously captured restart context", () => {
+ const first = own();
+ const root = { pid: 100, parentPid: 50, createdAt: "fixture", executable: "ChatGPT.exe", commandLine: `ChatGPT.exe --proxy-pac-url=${pac}` };
+ const context = captureWindowsCompatibilityContext([root]), io = executor();
+ first.release();
+ expect(() => windowsDesktopAppAdapter.relaunch(io.exec, install, context)).toThrow("desktop_compatibility_launch_owner_unverified");
+ const second = own();
+ expect(second.generation).not.toBe(first.generation);
+ expect(() => windowsDesktopAppAdapter.relaunch(io.exec, install, context)).toThrow("desktop_compatibility_launch_owner_unverified");
+ expect(io.calls).toEqual([]);
+ windowsDesktopAppAdapter.relaunch(io.exec, install, captureWindowsCompatibilityContext([root]));
+ expect(io.calls).toHaveLength(1);
+ });
+
+ test("accepts only canonical owned-shape loopback PAC URLs", () => {
+ expect(validatedCompatibilityPacUrl(pac)).toBe(pac);
+ for (const value of ["https://127.0.0.1:10102/fixture-0123456789/proxy.pac", pac.replace("127.0.0.1", "localhost"),
+ pac + "?other=1", pac + "#fragment", pac.replace("127.0.0.1", "user@127.0.0.1"), pac.replace("proxy.pac", "other.pac"),
+ pac.replace("fixture-0123456789", "short"), pac + " --disable-web-security", pac.replace(":10102", "")]) {
+ expect(() => validatedCompatibilityPacUrl(value)).toThrow("desktop_compatibility_invalid_pac_url");
+ }
+ });
+
+ test("uses Windows package activation with PAC arguments and requires package readback", () => {
+ const io = executor();
+ expect(launchWindowsCodexCompatibility(pac, { exec: io.exec, platform: "win32" })).toEqual({ status: "started", pid: 123, packageFullName });
+ const script = io.calls.at(-1)!;
+ expect(script).toContain("::Activate($family+'!App','--proxy-pac-url='+$pac)");
+ expect(script).toContain("::PackageOf($pidValue)");
+ expect(script).toContain("Get-AppxPackageManifest");
+ expect(script).toContain("Launch arguments not observed");
+ expect(script).not.toContain("Start-Process");
+ expect(script).not.toContain("taskkill");
+ });
+
+ test("an existing app or failed process discovery refuses before activation", () => {
+ const running = executor({ running: `100 50 2026-01-01T00:00:00Z ${install.root}\\app\\ChatGPT.exe` });
+ expect(launchWindowsCodexCompatibility(pac, { exec: running.exec, platform: "win32" })).toEqual({ status: "refused", reason: "app_running" });
+ expect(running.calls.some(script => script.includes("OpenCodexPackageActivation"))).toBe(false);
+ const failed = executor({ probeFailure: true });
+ expect(launchWindowsCodexCompatibility(pac, { exec: failed.exec, platform: "win32" })).toEqual({ status: "refused", reason: "process_probe_failed" });
+ });
+
+ test("unverified activation or the wrong package publisher cannot report success", () => {
+ for (const output of ["broken", JSON.stringify({ pid: 123, packageFullName, verified: false }),
+ JSON.stringify({ pid: 123, packageFullName: "OpenAI.Codex_1.2.3.0_x64__foreign", verified: true })]) {
+ const io = executor({ output });
+ expect(launchWindowsCodexCompatibility(pac, { exec: io.exec, platform: "win32" })).toEqual({ status: "refused", reason: "activation_failed" });
+ }
+ });
+
+ test("non-Windows and armed tests never reach the real native app", () => {
+ expect(launchWindowsCodexCompatibility(pac, { platform: "linux" })).toEqual({ status: "refused", reason: "unsupported_platform" });
+ expect(launchWindowsCodexCompatibility(pac, { platform: "win32" })).toEqual({ status: "refused", reason: "test_environment" });
+ });
+
+ 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");
+ }, 15_000);
+});
diff --git a/tests/clients/desktop-compatibility-native-identity.test.ts b/tests/clients/desktop-compatibility-native-identity.test.ts
new file mode 100644
index 00000000000..aecdc2b2da9
--- /dev/null
+++ b/tests/clients/desktop-compatibility-native-identity.test.ts
@@ -0,0 +1,100 @@
+import { afterEach, expect, test } from "bun:test";
+import { mkdtempSync, rmdirSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createNativeIdentityReader } from "../../src/codex/desktop-compatibility/native-identity";
+import { UsageRelayController } from "../../src/codex/desktop-compatibility/usage-controller";
+
+const roots: string[] = [];
+afterEach(() => { for (const root of roots.splice(0)) { unlinkSync(join(root, "auth.json")); rmdirSync(root); } });
+function fixture() {
+ const root = mkdtempSync(join(tmpdir(), "desktop-identity-")); roots.push(root);
+ const path = join(root, "auth.json"); let revision = 0;
+ const write = (id = "fixture-A", access = "synthetic-access-A") => {
+ const claims = { "https://api.openai.com/auth": { chatgpt_account_id: id, chatgpt_user_id: "fixture-user", chatgpt_plan_type: "pro" } };
+ writeFileSync(path, JSON.stringify({ auth_mode: "chatgpt", tokens: { account_id: id, access_token: access,
+ id_token: `fixture.${Buffer.from(JSON.stringify(claims)).toString("base64url")}.fixture` } }));
+ const timestamp = new Date(Date.now() + ++revision * 2000); utimesSync(path, timestamp, timestamp);
+ };
+ write(); return { path, write };
+}
+const usage = { account_id: "fixture-A", user_id: "fixture-user", plan_type: "pro",
+ rate_limit: { allowed: false, limit_reached: true, primary_window: { used_percent: 100 } },
+ spend_control: { reached: false }, credits: { has_credits: false, unlimited: false } };
+function upstreamFixture(beforeResponse?: () => Promise) {
+ const requests: Request[] = [];
+ return {
+ fetch: (async (input, init) => {
+ requests.push(new Request(input, init)); await beforeResponse?.(); return Response.json(usage);
+ }) as typeof fetch,
+ verify(tokens: string[]) {
+ // Assert outside the production reader's catch: a bad request must not look
+ // like the expected null result of a negative identity test.
+ expect(requests).toHaveLength(tokens.length);
+ requests.forEach((request, index) => {
+ expect(request.url).toBe("https://chatgpt.com/backend-api/wham/usage");
+ expect(request.method).toBe("GET"); expect(request.redirect).toBe("manual");
+ expect(request.headers.get("authorization")).toBe(`Bearer ${tokens[index]}`);
+ expect(request.headers.get("ChatGPT-Account-ID")).toBe("fixture-A");
+ });
+ },
+ };
+}
+const exchange = { method: "GET", pathname: "/backend-api/wham/usage", status: 200 };
+const consent = { scope: "account-ui-compatibility" as const, accountWideConsent: true };
+
+test("unchanged native credentials keep one opaque generation and verify without exposing tokens", async () => {
+ const io = fixture(), upstream = upstreamFixture(), reader = createNativeIdentityReader(io.path, upstream.fetch);
+ const first = await reader.readCurrentIdentity();
+ expect(first?.credentialGeneration).toBeDefined();
+ expect(await reader.readCurrentIdentity()).toEqual(first);
+ expect(await reader.verifyFreshIdentity()).toEqual(first);
+ expect(JSON.stringify(first)).not.toContain("synthetic-access");
+ expect(JSON.stringify(first)).not.toContain("id_token");
+ upstream.verify(["synthetic-access-A"]);
+});
+
+for (const change of ["token rotation", "A to B to A"] as const) test(`a delayed identity response cannot cross ${change}`, async () => {
+ const io = fixture(); let entered!: () => void, release!: () => void;
+ const started = new Promise(resolve => { entered = resolve; });
+ const pending = new Promise(resolve => { release = resolve; });
+ const upstream = upstreamFixture(async () => { entered(); await pending; });
+ const reader = createNativeIdentityReader(io.path, upstream.fetch);
+ const before = await reader.readCurrentIdentity(), verifying = reader.verifyFreshIdentity(); await started;
+ if (change === "token rotation") io.write("fixture-A", "synthetic-access-new");
+ else { io.write("fixture-B", "synthetic-access-B"); io.write(); }
+ release(); expect(await verifying).toBeNull();
+ expect((await reader.readCurrentIdentity())?.credentialGeneration).not.toBe(before?.credentialGeneration);
+ upstream.verify(["synthetic-access-A"]);
+});
+
+test("an unreadable intermediate login cannot recover an old credential generation", async () => {
+ const io = fixture(), upstream = upstreamFixture(), reader = createNativeIdentityReader(io.path, upstream.fetch), before = await reader.readCurrentIdentity();
+ writeFileSync(io.path, "{}"); expect(await reader.readCurrentIdentity()).toBeNull(); io.write();
+ expect((await reader.readCurrentIdentity())?.credentialGeneration).not.toBe(before?.credentialGeneration);
+ upstream.verify([]);
+});
+
+test("a trial disarms on native credential replacement and a fresh runtime can bind the replacement", async () => {
+ const io = fixture(), upstream = upstreamFixture(), reader = createNativeIdentityReader(io.path, upstream.fetch), account = (await reader.verifyFreshIdentity())!;
+ const controller = new UsageRelayController(account, reader.readCurrentIdentity, reader.verifyFreshIdentity, Date.now, Date.now() + 600000);
+ await controller.rewriteJson(JSON.stringify(usage), exchange); expect((await controller.activate(consent)).accepted).toBe(true);
+ io.write("fixture-A", "synthetic-access-new");
+ expect(await controller.rewriteJson(JSON.stringify(usage), exchange)).toBeNull();
+ expect(controller.snapshot().mode).toBe("observe"); expect(controller.snapshot().outputs).toBe(0);
+ const replacement = new UsageRelayController((await reader.verifyFreshIdentity())!, reader.readCurrentIdentity, reader.verifyFreshIdentity, Date.now, Date.now() + 600000);
+ await replacement.rewriteJson(JSON.stringify(usage), exchange); expect((await replacement.activate(consent)).accepted).toBe(true);
+ upstream.verify(["synthetic-access-A", "synthetic-access-A", "synthetic-access-new", "synthetic-access-new"]);
+});
+
+test("A to B to A during the async build check cannot publish a corrected old response", async () => {
+ const io = fixture(), upstream = upstreamFixture(), reader = createNativeIdentityReader(io.path, upstream.fetch); let waiting = false, entered!: () => void, release!: (value: boolean) => void;
+ const started = new Promise(resolve => { entered = resolve; });
+ const controller = new UsageRelayController((await reader.verifyFreshIdentity())!, reader.readCurrentIdentity, reader.verifyFreshIdentity, Date.now, Date.now() + 600000, 180000,
+ () => waiting ? (entered(), new Promise(resolve => { release = resolve; })) : true);
+ await controller.rewriteJson(JSON.stringify(usage), exchange); await controller.activate(consent);
+ waiting = true; const correcting = controller.rewriteJson(JSON.stringify(usage), exchange); await started;
+ io.write("fixture-B", "synthetic-access-B"); io.write(); release(true);
+ expect(await correcting).toBeNull(); expect(controller.snapshot().mode).toBe("observe"); expect(controller.snapshot().outputs).toBe(0);
+ upstream.verify(["synthetic-access-A", "synthetic-access-A"]);
+});
diff --git a/tests/clients/desktop-compatibility-relay.test.ts b/tests/clients/desktop-compatibility-relay.test.ts
new file mode 100644
index 00000000000..0fb765666f6
--- /dev/null
+++ b/tests/clients/desktop-compatibility-relay.test.ts
@@ -0,0 +1,213 @@
+import { expect, test } from "bun:test";
+import { createServer, request } from "node:https";
+import { connect } from "node:tls";
+import type { Duplex } from "node:stream";
+import { createCertificateAuthority, issueServerLeaf } from "../../src/claude/intercept/local-ca";
+import { startDesktopRelay } from "../../src/codex/desktop-compatibility/relay-listener";
+import { forwardProxy } from "../helpers/desktop-egress-fixture";
+import { UsageRelayController } from "../../src/codex/desktop-compatibility/usage-controller";
+import { createUsageControlledFetch } from "../../src/codex/desktop-compatibility/usage-controlled-fetch";
+import { controlledUsageSse } from "../../src/codex/desktop-compatibility/usage-sse-controller";
+
+test("oversized SSE records pass through byte-for-byte and framing resumes at the next record", async () => {
+ for (const lineEnding of ["\n", "\r", "\r\n"]) {
+ const oversized = Buffer.from(`event: usage.snapshot${lineEnding}data: {"noise":"${"x".repeat(160)}"}${lineEnding}${lineEnding}`);
+ const later = Buffer.from(`event: usage.snapshot${lineEnding}data: {"marker":"normal"}${lineEnding}${lineEnding}`);
+ const input = Buffer.concat([oversized, later]);
+ const calls: string[] = [];
+ const source = new ReadableStream({
+ start(controller) {
+ for (const byte of input) controller.enqueue(Uint8Array.of(byte));
+ controller.close();
+ },
+ });
+ const output = await new Response(source.pipeThrough(controlledUsageSse(async text => {
+ calls.push(text);
+ return text === '{"marker":"normal"}' ? '{"marker":"rewritten"}' : null;
+ }, 64))).arrayBuffer();
+ const bytes = Buffer.from(output);
+
+ expect(bytes.subarray(0, oversized.length)).toEqual(oversized);
+ expect(bytes.subarray(oversized.length)).toEqual(Buffer.from(
+ `event: usage.snapshot${lineEnding}data: {"marker":"rewritten"}${lineEnding}${lineEnding}`));
+ expect(calls).toEqual(['{"marker":"normal"}']);
+ }
+});
+
+test("an oversized usage record is preserved through the 200 relay and a later record is still rewritten", async () => {
+ const account = { id: "fixture-account", userId: "fixture-user", plan: "pro", structure: "personal" } as const;
+ const usage = { account_id: account.id, user_id: account.userId, plan_type: account.plan,
+ rate_limit: { allowed: false, limit_reached: true, primary_window: { used_percent: 100, reset_at: 123456 } },
+ spend_control: { reached: false }, credits: { has_credits: false, unlimited: false } };
+ const controller = new UsageRelayController(account, async () => account, async () => account, () => 1000, 100000);
+ await controller.rewriteJson(JSON.stringify(usage), { method: "GET", pathname: "/backend-api/wham/usage", status: 200 });
+ expect((await controller.activate({ scope: "account-ui-compatibility", accountWideConsent: true })).accepted).toBe(true);
+
+ const oversizedUsage = { ...usage, observation_noise: "x".repeat(262200) };
+ const oversized = Buffer.from(`event: usage.snapshot\r\ndata: ${JSON.stringify({ version: 1, stream_id: "fixture", sequence: 1, usage: oversizedUsage })}\r\n\r\n`);
+ const later = Buffer.from(`event: usage.snapshot\r\ndata: ${JSON.stringify({ version: 1, stream_id: "fixture", sequence: 2, usage })}\r\n\r\n`);
+ const corrected = Buffer.from(`event: usage.snapshot\r\ndata: ${JSON.stringify({ version: 1, stream_id: "fixture", sequence: 2,
+ usage: { ...usage, rate_limit: { ...usage.rate_limit, allowed: true, limit_reached: false } } })}\r\n\r\n`);
+ const body = Buffer.concat([oversized, later]);
+ const chunkSizes = [65533, 19, 131071, 2, 7, 16383, 1, 4093];
+ const source = new ReadableStream({
+ start(stream) {
+ let offset = 0, index = 0;
+ while (offset < body.length) {
+ const end = Math.min(body.length, offset + chunkSizes[index++ % chunkSizes.length]!);
+ stream.enqueue(body.subarray(offset, end));
+ offset = end;
+ }
+ stream.close();
+ },
+ });
+ const ca = createCertificateAuthority({ commonName: "oversized-usage-fixture", validityDays: 1 });
+ const relay = await startDesktopRelay({ leaf: issueServerLeaf(ca, "oversized-usage-fixture", ["chatgpt.com"]),
+ fetchImpl: createUsageControlledFetch(controller, (async () => new Response(source, {
+ headers: { "content-type": "text/event-stream" },
+ })) as typeof fetch) });
+ try {
+ const response = await new Promise<{ status?: number; complete: boolean; bytes: Buffer }>((resolve, reject) => {
+ const client = request({ hostname: "127.0.0.1", port: relay.port, servername: "chatgpt.com", ca: ca.certPem,
+ path: "/backend-api/wham/usage/stream", headers: { host: "chatgpt.com" } }, incoming => {
+ const chunks: Buffer[] = [];
+ incoming.on("data", chunk => chunks.push(Buffer.from(chunk)));
+ incoming.once("error", reject);
+ incoming.once("end", () => resolve({ status: incoming.statusCode, complete: incoming.complete, bytes: Buffer.concat(chunks) }));
+ });
+ client.once("error", reject);
+ client.setTimeout(10000, () => client.destroy(new Error("fixture timeout")));
+ client.end();
+ });
+ expect(response.status).toBe(200);
+ expect(response.complete).toBe(true);
+ expect(response.bytes).toEqual(Buffer.concat([oversized, corrected]));
+ expect(controller.snapshot().observation.streamSnapshots).toBe(1);
+ expect(controller.snapshot().outputs).toBe(1);
+ } finally { await relay.close(); await controller.observeOnly(); }
+});
+
+test("HTTP Host is case-insensitive while foreign hosts and ports cannot reach upstream", async () => {
+ const ca = createCertificateAuthority({ commonName: "host-fixture", validityDays: 1 });
+ let forwarded = 0;
+ const relay = await startDesktopRelay({ leaf: issueServerLeaf(ca, "host-fixture", ["chatgpt.com"]),
+ fetchImpl: (async input => { expect(String(input)).toBe("https://chatgpt.com/fixture"); forwarded++; return new Response("fixture"); }) as typeof fetch });
+ try {
+ for (const [host, status] of [["CHATGPT.COM", 200], ["ChatGPT.Com:443", 200], ["chatgpt.com:444", 421],
+ ["chatgpt.com.evil", 421], ["chatgpt.com.", 421]] as const) {
+ const actual = await new Promise((resolve, reject) => {
+ const client = request({ hostname: "127.0.0.1", port: relay.port, servername: "chatgpt.com", ca: ca.certPem,
+ path: "/fixture", headers: { host } }, response => { response.resume(); response.once("end", () => resolve(response.statusCode)); response.once("error", reject); });
+ client.once("error", reject); client.setTimeout(5000, () => client.destroy(new Error("fixture timeout"))); client.end();
+ });
+ expect(actual).toBe(status);
+ }
+ expect(forwarded).toBe(2);
+ } finally { await relay.close(); }
+}, 10000);
+
+for (const route of ["direct", "http", "https", "socks5"] as const) test(`upgraded native app traffic preserves handshake and raw frames through ${route}`, async () => {
+ const ca = createCertificateAuthority({ commonName: "relay-fixture", validityDays: 1 });
+ const leaf = issueServerLeaf(ca, "relay-fixture", ["chatgpt.com"]);
+ const upstream = createServer({ cert: leaf.certPem, key: leaf.keyPem });
+ const sockets = new Set();
+ let cookie: string | undefined, protocol: string | undefined;
+ const frame = Buffer.from([0x82, 0x83, 0x01, 0x02, 0x03, 0x04, 0x7a, 0x00, 0xff]);
+ upstream.on("connection", socket => { sockets.add(socket); socket.once("close", () => sockets.delete(socket)); });
+ upstream.on("upgrade", (req, client, head) => {
+ cookie = req.headers.cookie; protocol = req.headers["sec-websocket-protocol"] as string | undefined;
+ client.on("error", () => {});
+ client.write("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Protocol: fixture.v1\r\n\r\n");
+ if (head.length) client.write(head);
+ client.on("data", data => client.write(data));
+ });
+ await new Promise(resolve => upstream.listen(0, "127.0.0.1", resolve));
+ const port = (upstream.address() as { port: number }).port;
+ const proxy = route === "direct" ? null : await forwardProxy(route, port, issueServerLeaf(ca, "relay-fixture", ["localhost"]));
+ const relay = await startDesktopRelay({ leaf, fetchImpl: (async () => { throw new Error("Upgrade must not use HTTP fetch"); }) as typeof fetch,
+ websocketPeer: { host: "127.0.0.1", port, ca: ca.certPem }, websocketTransport: { proxy: proxy?.url ?? false, proxyCa: ca.certPem } });
+ const client = connect({ host: "127.0.0.1", port: relay.port, servername: "chatgpt.com", ca: ca.certPem });
+ try {
+ const bytes = await new Promise((resolve, reject) => {
+ let result = Buffer.alloc(0);
+ client.setTimeout(5000, () => reject(new Error("fixture timeout"))); client.once("error", reject);
+ client.on("data", chunk => {
+ result = Buffer.concat([result, chunk]); const end = result.indexOf("\r\n\r\n");
+ if (end !== -1 && result.length >= end + 4 + frame.length) resolve(result);
+ });
+ client.once("secureConnect", () => {
+ client.write("GET /dictation/stream HTTP/1.1\r\nHost: CHATGPT.COM\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: Zml4dHVyZQ==\r\nSec-WebSocket-Protocol: fixture.v1\r\nCookie: fixture=session\r\n\r\n");
+ client.write(frame);
+ });
+ });
+ expect(bytes.toString("latin1")).toContain("101 Switching Protocols");
+ expect(bytes.subarray(bytes.indexOf("\r\n\r\n") + 4)).toEqual(frame);
+ expect(cookie).toBe("fixture=session"); expect(protocol).toBe("fixture.v1");
+ if (proxy) expect(proxy.seen).toHaveLength(1);
+ } finally {
+ client.destroy(); await relay.close();
+ await proxy?.close();
+ for (const socket of sockets) socket.destroy();
+ await new Promise(resolve => upstream.close(() => resolve()));
+ }
+}, 10000);
+
+test("conversation initialization restrictions remain untouched even during a usage trial", async () => {
+ const account = { id: "fixture-account", userId: "fixture-user", plan: "pro", structure: "personal" } as const;
+ const controller = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000);
+ await controller.rewriteJson(JSON.stringify({ account_id: account.id, user_id: account.userId, plan_type: "pro",
+ rate_limit: { allowed: false, limit_reached: true }, spend_control: { reached: false }, credits: { has_credits: false, unlimited: false } }),
+ { method: "GET", pathname: "/backend-api/wham/usage", status: 200 });
+ expect((await controller.activate({ scope: "account-ui-compatibility", accountWideConsent: true })).accepted).toBe(true);
+ const payload = JSON.stringify({ blocked_features: ["fixture-policy"], limits_progress: { fixture_limit: 100 },
+ rate_limit: { allowed: false, limit_reached: true } });
+ for (const method of ["GET", "POST"]) {
+ const upstream = new Response(payload, { headers: { "content-type": "application/json", etag: '"original"' } });
+ const forward = createUsageControlledFetch(controller, (async () => upstream) as typeof fetch);
+ const response = await forward("https://chatgpt.com/backend-api/conversation/init", { method });
+ expect(response).toBe(upstream); expect(await response.text()).toBe(payload); expect(response.headers.get("etag")).toBe('"original"');
+ }
+ expect(controller.snapshot().outputs).toBe(0);
+});
+
+for (const mode of ["observe", "apply"] as const) test(`attachment upload stays byte-identical in ${mode} mode`, async () => {
+ const account = { id: "fixture-account", userId: "fixture-user", plan: "pro", structure: "personal" } as const;
+ const controller = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000);
+ if (mode === "apply") {
+ await controller.rewriteJson(JSON.stringify({ account_id: account.id, user_id: account.userId, plan_type: "pro",
+ rate_limit: { allowed: false, limit_reached: true, primary_window: { used_percent: 100, reset_at: 123456 } },
+ spend_control: { reached: false }, credits: { has_credits: false, unlimited: false } }),
+ { method: "GET", pathname: "/backend-api/wham/usage", status: 200 });
+ expect((await controller.activate({ scope: "account-ui-compatibility", accountWideConsent: true })).accepted).toBe(true);
+ }
+ const ca = createCertificateAuthority({ commonName: "attachment-fixture", validityDays: 1 });
+ const contentType = "multipart/form-data; boundary=fixture-upload";
+ const upload = Buffer.concat([Buffer.from('--fixture-upload\r\nContent-Disposition: form-data; name="file"; filename="fixture.bin"\r\nContent-Type: application/octet-stream\r\n\r\n'),
+ Buffer.from([0, 0xff, 0x80, 0x0d, 0x0a]), Buffer.from('첨부 UTF-8 {"rate_limit":{"allowed":false}}\r\n--fixture-upload--\r\n')]);
+ const download = Buffer.concat([Buffer.from([0, 0xff, 0x80]), Buffer.from('{"rate_limit":{"allowed":false,"limit_reached":true}}')]);
+ let captured: { url: string; method?: string; headers: Headers; bytes: Buffer } | undefined;
+ const upstream = (async (input, init) => {
+ captured = { url: String(input), method: init?.method, headers: new Headers(init?.headers), bytes: Buffer.from(await new Response(init?.body).arrayBuffer()) };
+ const headers = new Headers({ "content-type": "application/octet-stream" });
+ headers.append("set-cookie", "fixture-a=one; Secure"); headers.append("set-cookie", "fixture-b=two; Secure");
+ return new Response(download, { status: 201, headers });
+ }) as typeof fetch;
+ const relay = await startDesktopRelay({ leaf: issueServerLeaf(ca, "attachment-fixture", ["chatgpt.com"]), fetchImpl: createUsageControlledFetch(controller, upstream) });
+ try {
+ const response = await new Promise<{ status?: number; cookies?: string[]; bytes: Buffer }>((resolve, reject) => {
+ const client = request({ hostname: "127.0.0.1", port: relay.port, servername: "chatgpt.com", ca: ca.certPem,
+ path: "/backend-api/files", method: "POST", headers: { host: "chatgpt.com", "content-type": contentType,
+ "content-length": upload.length, authorization: "Bearer fixture-token", cookie: "fixture=session" } }, incoming => {
+ const chunks: Buffer[] = []; incoming.on("data", chunk => chunks.push(Buffer.from(chunk))); incoming.once("error", reject);
+ incoming.once("end", () => resolve({ status: incoming.statusCode, cookies: incoming.headers["set-cookie"], bytes: Buffer.concat(chunks) }));
+ });
+ client.once("error", reject); client.setTimeout(5000, () => client.destroy(new Error("fixture timeout"))); client.end(upload);
+ });
+ expect(captured?.url).toBe("https://chatgpt.com/backend-api/files"); expect(captured?.method).toBe("POST");
+ expect(captured?.bytes).toEqual(upload); expect(captured?.headers.get("content-type")).toBe(contentType);
+ expect(captured?.headers.get("authorization")).toBe("Bearer fixture-token"); expect(captured?.headers.get("cookie")).toBe("fixture=session");
+ expect(response.status).toBe(201); expect(response.bytes).toEqual(download);
+ expect(response.cookies).toEqual(["fixture-a=one; Secure", "fixture-b=two; Secure"]);
+ expect(controller.snapshot().mode).toBe(mode); expect(controller.snapshot().outputs).toBe(0);
+ } finally { await relay.close(); await controller.observeOnly(); }
+}, 10000);
diff --git a/tests/clients/desktop-compatibility-routing.test.ts b/tests/clients/desktop-compatibility-routing.test.ts
new file mode 100644
index 00000000000..547d982f70a
--- /dev/null
+++ b/tests/clients/desktop-compatibility-routing.test.ts
@@ -0,0 +1,111 @@
+import { afterEach, expect, test } from "bun:test";
+import { mkdtempSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createNativeRoutingVerifier, matchesNativeCompatibilityRouting } from "../../src/codex/desktop-compatibility/routing-preflight";
+import { bindNativeCompatibilityOwner, nativeCompatibilityOwner, type NativeCompatibilityOwner } from "../../src/codex/desktop-compatibility/routing-binding";
+import { removeTreeWithRetry } from "../helpers/remove-tree";
+const owner: NativeCompatibilityOwner = { hostname: "127.0.0.1", port: 12001, loopbackPort: 12002, config: { port: 10100, providers: {} } };
+const text = 'model_provider = "openai"\nopenai_base_url = "http://127.0.0.1:12001/v1"\n';
+const roots: string[] = [];
+afterEach(() => { for (const root of roots.splice(0)) removeTreeWithRetry(root); });
+
+test("address family matches the bound listener instead of any loopback alias", () => {
+ const ipv6 = text.replace("127.0.0.1", "[::1]");
+ expect(matchesNativeCompatibilityRouting(ipv6, owner)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text.replace("127.0.0.1", "localhost"), owner)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text, { ...owner, hostname: "0.0.0.0" })).toBe(true);
+ expect(matchesNativeCompatibilityRouting(text, { ...owner, hostname: "192.0.2.10" })).toBe(false);
+ const v6Owner = { ...owner, hostname: "::1" };
+ expect(matchesNativeCompatibilityRouting(ipv6, v6Owner)).toBe(true);
+ expect(matchesNativeCompatibilityRouting(text, v6Owner)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text.replace(":12001", ":12002"), v6Owner)).toBe(true);
+ expect(matchesNativeCompatibilityRouting(ipv6.replace(":12001", ":12002"), v6Owner)).toBe(false);
+ // Mutable desired config does not replace the socket's captured address.
+ expect(matchesNativeCompatibilityRouting(ipv6, { ...owner, config: { ...owner.config, hostname: "::1" } })).toBe(false);
+});
+
+test("changing the actual listener identity revokes a prior observation", () => {
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-listener-change-")); roots.push(root);
+ writeFileSync(join(root, "config.toml"), text);
+ const live = { ...owner }, verify = createNativeRoutingVerifier(root, () => live);
+ expect(verify()).toBe(true);
+ live.hostname = "0.0.0.0";
+ expect(matchesNativeCompatibilityRouting(text, live)).toBe(true);
+ expect(verify()).toBe(false);
+});
+test("the actual bound listener and companion port are accepted, not a stale configured port", () => {
+ expect(matchesNativeCompatibilityRouting(text, owner)).toBe(true);
+ expect(matchesNativeCompatibilityRouting(text.replace(":12001", ":12002"), owner)).toBe(true);
+ expect(matchesNativeCompatibilityRouting(text.replace(":12001", ":10100"), owner)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text, null)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text, { ...owner, config: { ...owner.config, codexDesktopAuthless: true } })).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text, { ...owner, config: { ...owner.config, runtimeRole: "client" } })).toBe(false);
+});
+test("foreign providers, destinations, credentials and unknown profiles cannot qualify", () => {
+ for (const candidate of [text.replace('"openai"', '"custom"'), text.replace("127.0.0.1", "example.test"),
+ text.replace("http://", "https://"), text.replace("127.0.0.1", "user:pass@127.0.0.1"), text.replace("/v1", "/v1?x=1"),
+ text + 'profile = "missing"', 'invalid toml [', text + 'forced_login_method = "api"']) {
+ expect(matchesNativeCompatibilityRouting(candidate, owner)).toBe(false);
+ }
+ expect(matchesNativeCompatibilityRouting(text + 'profile = "work"\n[profiles.work]\nmodel_provider = "custom"', owner)).toBe(false);
+ expect(matchesNativeCompatibilityRouting(text + 'profile = "work"\n[profiles.work]\nmodel_provider = "openai"', owner)).toBe(true);
+});
+test("read-only verifier rereads changed config and owner detach cannot clear a newer owner", () => {
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-routing-")); roots.push(root);
+ const path = join(root, "config.toml"); writeFileSync(path, text);
+ const detach = bindNativeCompatibilityOwner(owner), second = { ...owner };
+ const detachSecond = bindNativeCompatibilityOwner(second); detach(); expect(nativeCompatibilityOwner()).toBe(second);
+ try {
+ const verify = createNativeRoutingVerifier(root); expect(verify()).toBe(true);
+ writeFileSync(path, text.replace(":12001", ":13000")); expect(verify()).toBe(false);
+ } finally { detachSecond(); }
+ expect(nativeCompatibilityOwner()).toBeNull();
+});
+
+test("model and fallback edits revoke routing until a new observation context is created", () => {
+ const changes = [
+ { defaultProvider: "openai" }, { subagentModelFallback: ["openai/gpt-6-luna"] },
+ { subagentModelFallbackByModel: { "external/model": ["openai/gpt-6-luna"] } },
+ { compactionRecovery: { enabled: true, model: "openai/gpt-6-luna" } },
+ { blockedModelRedirects: { "external/model": "openai/gpt-6-luna" } },
+ { providers: { external: { baseUrl: "https://changed.example.test/v1" } } },
+ { combos: {} }, { routingProfiles: {} },
+ { memoryModels: { extract: { model: "openai/gpt-6-luna" } } },
+ ];
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-route-change-")); roots.push(root);
+ writeFileSync(join(root, "config.toml"), text);
+ for (const change of changes) {
+ const live = { ...owner, config: { ...owner.config } };
+ const verify = createNativeRoutingVerifier(root, () => live); expect(verify()).toBe(true);
+ Object.assign(live.config, change); expect(verify()).toBe(false);
+ live.config = { ...owner.config }; expect(verify()).toBe(false);
+ expect(createNativeRoutingVerifier(root, () => live)()).toBe(true);
+ }
+});
+
+test("root model edits revoke context but unrelated preference and object order do not", () => {
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-route-baseline-")); roots.push(root);
+ const path = join(root, "config.toml"); writeFileSync(path, text);
+ const live = { ...owner, config: { ...owner.config, blockedModelRedirects: { a: "external/a", b: "external/b" } } };
+ const verify = createNativeRoutingVerifier(root, () => live); expect(verify()).toBe(true);
+ live.config.blockedModelRedirects = { b: "external/b", a: "external/a" };
+ Object.assign(live.config, { desktopCompatibility: { startOnProxyStart: true } }); expect(verify()).toBe(true);
+ writeFileSync(path, text + 'model = "external/other"\n'); expect(verify()).toBe(false);
+ writeFileSync(path, text); expect(verify()).toBe(false);
+});
+
+test("desktop MCP rewrites and TOML formatting preserve the observation while model edits still revoke it", () => {
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-mcp-refresh-")); roots.push(root);
+ const path = join(root, "config.toml");
+ writeFileSync(path, text + 'model = "external/model"\n[mcp_servers.desktop]\nurl = "http://127.0.0.1:21001/mcp"\n');
+ const verify = createNativeRoutingVerifier(root, () => owner);
+ expect(verify()).toBe(true);
+ const rewritten = '# Desktop rewrites its MCP endpoint after launch.\n' + text
+ + 'model = "external/model"\n[mcp_servers.desktop]\nurl = "http://127.0.0.1:21002/mcp"\n';
+ writeFileSync(path, rewritten); expect(verify()).toBe(true);
+ writeFileSync(path, text + 'model = "external/model"\n'); expect(verify()).toBe(true);
+ writeFileSync(path, rewritten.replace('model = "external/model"', 'model = "openai/model"'));
+ expect(verify()).toBe(false);
+ writeFileSync(path, rewritten); expect(verify()).toBe(false);
+});
diff --git a/tests/clients/desktop-compatibility-runtime.test.ts b/tests/clients/desktop-compatibility-runtime.test.ts
new file mode 100644
index 00000000000..34693897bcb
--- /dev/null
+++ b/tests/clients/desktop-compatibility-runtime.test.ts
@@ -0,0 +1,352 @@
+import { UsageActivation } from "../../src/codex/desktop-compatibility/usage-activation";
+import { afterEach, describe, expect, test } from "bun:test";
+import { createCertificateAuthority } from "../../src/claude/intercept/local-ca";
+import { X509Certificate } from "node:crypto";
+import { createDesktopCompatibilityRuntime } from "../../src/codex/desktop-compatibility/runtime";
+import { UsageRelayController, type UsageIdentity } from "../../src/codex/desktop-compatibility/usage-controller";
+import { desktopCompatibilityRuntimeActive, readDesktopCompatibilityLaunch } from "../../src/codex/desktop-compatibility/runtime-ownership";
+import { captureWindowsCompatibilityContext, assertWindowsCompatibilityContext } from "../../src/codex/desktop-compatibility/windows-package-command";
+import { createDesktopCertificateService } from "../../src/codex/desktop-compatibility/certificate-service";
+import { resetOptionalShutdownHooksForTests, runOptionalShutdownHooks } from "../../src/lib/optional-shutdown-hooks";
+import type { DesktopConnectionIdentity, DesktopConnectionStore } from "../../src/codex/desktop-compatibility/connection-store";
+import { createServer as createTcpServer } from "node:net";
+
+const account: UsageIdentity = { id: "fixture-account", userId: "fixture-user", plan: "pro", structure: "personal" };
+const usage = { account_id: account.id, user_id: account.userId, plan_type: "pro", rate_limit: { allowed: false, limit_reached: true,
+ primary_window: { used_percent: 100, reset_at: 123456 } }, spend_control: { reached: false }, credits: { has_credits: false, unlimited: false } };
+const exchange = { method: "GET", pathname: "/backend-api/wham/usage/stream", status: 200 };
+const frame = (data = usage, sequence = 7) => JSON.stringify({ version: 1, stream_id: "fixture", sequence, usage: data });
+const consent = { scope: "account-ui-compatibility" as const, accountWideConsent: true };
+const stopped: (() => Promise)[] = [];
+afterEach(async () => { for (const stop of stopped.splice(0)) await stop(); resetOptionalShutdownHooksForTests(); });
+
+describe("bounded compatibility usage controller", () => {
+ test("an open stream is not an observed account snapshot or proof of native composer recovery", async () => {
+ const ctl = new UsageRelayController(account, async () => account, async () => account, () => 1000, 100000);
+ const stream = ctl.registerStream(exchange, async () => {})!;
+ expect(ctl.snapshot().trackedStreams).toBe(1);
+ expect(ctl.snapshot().observation).toEqual({ jsonSnapshots: 0, streamSnapshots: 0, validatedActiveStreams: 0,
+ lastSnapshotAt: null, sourceProcessVerified: false, composerRecoveryVerified: false });
+ await ctl.rewriteJson(frame({ ...usage, account_id: "foreign" }), exchange, stream);
+ expect(ctl.snapshot().observation.streamSnapshots).toBe(0);
+ const validStream = ctl.registerStream(exchange, async () => {})!;
+ await ctl.rewriteJson(frame(), exchange, validStream);
+ await ctl.rewriteJson(JSON.stringify(usage), { ...exchange, pathname: "/backend-api/wham/usage" });
+ expect(ctl.snapshot().observation).toEqual({ jsonSnapshots: 1, streamSnapshots: 1, validatedActiveStreams: 1,
+ lastSnapshotAt: 1000, sourceProcessVerified: false, composerRecoveryVerified: false });
+ validStream.release();
+ expect(ctl.snapshot().observation.validatedActiveStreams).toBe(0);
+ expect(ctl.snapshot().observation.streamSnapshots).toBe(1);
+ });
+ for (const [label, replacement] of [
+ ["available", { ...usage, rate_limit: { ...usage.rate_limit, allowed: true, limit_reached: false } }],
+ ["protected", { ...usage, spend_control: { reached: true } }],
+ ] as const) test(`${label} usage ends the trial and later exhaustion cannot silently rearm it`, async () => {
+ const ctl = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000);
+ await ctl.rewriteJson(frame(), exchange); expect((await ctl.activate(consent)).accepted).toBe(true);
+ expect(await ctl.rewriteJson(frame(replacement), exchange)).toBeNull();
+ expect(ctl.snapshot().mode).toBe("observe"); expect(ctl.snapshot().outputs).toBe(0);
+ expect(await ctl.rewriteJson(frame(), exchange)).toBeNull();
+ expect(ctl.snapshot().mode).toBe("observe");
+ expect((await ctl.activate(consent)).accepted).toBe(true);
+ });
+ test("recovery supersedes an exhausted response waiting for its build check", async () => {
+ let waiting = false, entered!: () => void, release!: (value: boolean) => void;
+ const started = new Promise(resolve => { entered = resolve; });
+ const ctl = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000, 180000,
+ () => waiting ? (entered(), new Promise(resolve => { release = resolve; })) : true);
+ await ctl.rewriteJson(frame(), exchange); await ctl.activate(consent);
+ waiting = true; const pending = ctl.rewriteJson(frame(), exchange); await started;
+ await ctl.rewriteJson(frame({ ...usage, rate_limit: { ...usage.rate_limit, allowed: true, limit_reached: false } }), exchange);
+ release(true); expect(await pending).toBeNull(); expect(ctl.snapshot().mode).toBe("observe");
+ expect(ctl.snapshot().outputs).toBe(0);
+ });
+ test("returning to observation supersedes activation during its asynchronous context check", async () => {
+ let release!: (value: boolean) => void, entered!: () => void;
+ const waiting = new Promise(resolve => { entered = resolve; });
+ const ctl = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000, 180000,
+ () => { entered(); return new Promise(resolve => { release = resolve; }); });
+ await ctl.rewriteJson(frame(), exchange);
+ const activation = ctl.activate(consent); await waiting; await ctl.observeOnly(); release(true);
+ expect((await activation).accepted).toBe(false); expect(ctl.snapshot().mode).toBe("observe");
+ });
+ test("observation avoids build probes and pending async checks cannot cross a trial or account change", async () => {
+ for (const change of ["observe", "account", "expiry"] as const) {
+ let now = 1000, current: UsageIdentity | null = account, checks = 0, waiting = false;
+ let entered!: () => void, release!: (value: boolean) => void;
+ const enteredPromise = new Promise(resolve => { entered = resolve; });
+ const ctl = new UsageRelayController(account, async () => current, async () => current, () => now, 100000, 180000, () => {
+ checks++; if (!waiting) return true;
+ entered(); return new Promise(resolve => { release = resolve; });
+ });
+ expect(await ctl.rewriteJson(frame(), exchange)).toBeNull(); expect(checks).toBe(0);
+ expect((await ctl.activate(consent)).accepted).toBe(true); expect(checks).toBe(1);
+ waiting = true; const response = ctl.rewriteJson(frame(), exchange);
+ await enteredPromise;
+ // The event loop remains usable while a package query is pending.
+ let ticked = false; await new Promise(resolve => setTimeout(() => { ticked = true; resolve(); }, 0)); expect(ticked).toBe(true);
+ if (change === "observe") await ctl.observeOnly();
+ if (change === "account") current = null;
+ if (change === "expiry") now = 100001;
+ release(true); expect(await response).toBeNull(); expect(ctl.snapshot().outputs).toBe(0);
+ }
+ });
+ test("observes first, refreshes only bound streams and preserves actual quota values", async () => {
+ let closed = 0;
+ const ctl = new UsageRelayController(account, async () => account, async () => account, Date.now, Date.now() + 600000);
+ const stream = ctl.registerStream(exchange, async () => { closed++; });
+ expect((await ctl.activate(consent)).accepted).toBe(false);
+ expect(await ctl.rewriteJson(frame(), exchange, stream)).toBeNull();
+ expect((await ctl.activate({ ...consent, accountWideConsent: false })).accepted).toBe(false);
+ expect((await ctl.activate(consent)).accepted).toBe(true); expect(closed).toBe(1);
+ const result = JSON.parse((await ctl.rewriteJson(frame(), exchange))!);
+ expect(result).toEqual({ version: 1, stream_id: "fixture", sequence: 7, usage: { ...usage, rate_limit: { ...usage.rate_limit, allowed: true, limit_reached: false } } });
+ expect(ctl.snapshot().appCacheConfirmed).toBe(false);
+ await ctl.observeOnly(); expect(await ctl.rewriteJson(frame(), exchange)).toBeNull();
+ });
+ test("identity, spending restrictions, app responses and unknown stream schemas stay protected", async () => {
+ let identity: UsageIdentity | null = account;
+ const ctl = new UsageRelayController(account, async () => identity, async () => identity, Date.now, Date.now() + 600000);
+ await ctl.rewriteJson(frame(), exchange); await ctl.activate(consent);
+ for (const context of [{ ...exchange, method: "POST" }, { ...exchange, status: 401 }, { ...exchange, pathname: "/backend-api/conversation" }]) {
+ expect(await ctl.rewriteJson(frame(), context)).toBeNull();
+ }
+ expect(await ctl.rewriteJson(frame({ ...usage, spend_control: { reached: true } }), exchange)).toBeNull();
+ expect(await ctl.rewriteJson(frame(usage, 0), exchange)).toBeNull();
+ identity = { ...account, id: "changed" };
+ expect(await ctl.rewriteJson(frame(), exchange)).toBeNull(); expect(ctl.snapshot().mode).toBe("observe");
+ });
+ test("an async identity check cannot carry correction across the safety deadline", async () => {
+ let now = 1000, advance = false;
+ const ctl = new UsageRelayController(account, async () => { if (advance) now = 2000; return account; }, async () => account, () => now, 2000);
+ await ctl.rewriteJson(frame(), exchange); await ctl.activate(consent); advance = true;
+ expect(await ctl.rewriteJson(frame(), exchange)).toBeNull(); expect(ctl.snapshot().mode).toBe("observe");
+ });
+});
+
+function fixture(trusted = true, connectionStore?: DesktopConnectionStore, buildProbe?: () => boolean | Promise) {
+ const authority = createCertificateAuthority({ commonName: "runtime-fixture", validityDays: 1 });
+ const cert = new X509Certificate(authority.certPem);
+ const identity = { readCurrentIdentity: async () => account, verifyFreshIdentity: async () => account };
+ const calls: { path: string; method: string; bytes: number; cookie: string | null }[] = [];
+ let streamSequence = 0, cancelledStreams = 0, buildSupported = true, routingSupported = true;
+ let connection: DesktopConnectionIdentity | null = null;
+ const runtime = createDesktopCompatibilityRuntime({ platform: "win32", testOnly: true, identity, buildSupported: () => buildProbe ? buildProbe() : buildSupported,
+ routingSupported: () => routingSupported,
+ connectionStore: connectionStore ?? { read: () => connection, publish: async value => (connection ??= value) },
+ loadAuthority: async () => ({ authority, commonName: "runtime-fixture", fingerprint: cert.fingerprint256.replaceAll(":", ""),
+ expiresAt: Date.parse(cert.validTo), renewalDue: false, reused: true }), trust: async () => trusted ? "trusted" : "not-trusted",
+ upstreamFetch: (async (input, init) => {
+ const request = new Request(input, init);
+ const data = new Uint8Array(await request.arrayBuffer());
+ const path = new URL(request.url).pathname;
+ calls.push({ path, method: request.method, bytes: data.length, cookie: request.headers.get("cookie") });
+ if (path === "/backend-api/wham/usage") return Response.json(usage, { headers: { etag: '"original-fixture"' } });
+ if (path === "/backend-api/wham/usage/stream") return new Response(new ReadableStream({
+ start(controller) { controller.enqueue(new TextEncoder().encode("event: usage.snapshot\ndata: " + frame(usage, ++streamSequence) + "\n\n")); },
+ cancel() { cancelledStreams++; },
+ }), { headers: { "content-type": "text/event-stream" } });
+ return new Response(data, { headers: { "content-type": "application/octet-stream", "set-cookie": "fixture=kept; Secure" } });
+ }) as typeof fetch,
+ });
+ stopped.push(() => runtime.stop());
+ return { runtime, authority, calls, cancelledStreams: () => cancelledStreams, setBuildSupported: (value: boolean) => { buildSupported = value; }, setRoutingSupported: (value: boolean) => { routingSupported = value; } };
+}
+
+describe("optional native compatibility runtime", () => {
+ test("only a serving runtime attests PAC preservation and stop/start invalidates the captured generation", async () => {
+ const io = fixture();
+ expect(readDesktopCompatibilityLaunch()).toBeNull();
+ await io.runtime.start();
+ const pacUrl = io.runtime.getPacUrl()!;
+ expect((await fetch(pacUrl)).status).toBe(200);
+ const root = { pid: 100, parentPid: 50, createdAt: "fixture", executable: "ChatGPT.exe", commandLine: `ChatGPT.exe --proxy-pac-url=${pacUrl}` };
+ const captured = captureWindowsCompatibilityContext([root]);
+ expect(captured.codexCompatibilityGeneration).toBe(readDesktopCompatibilityLaunch()!.generation);
+ expect(() => assertWindowsCompatibilityContext(captured)).not.toThrow();
+ const stopping = io.runtime.stop();
+ expect(readDesktopCompatibilityLaunch()).toBeNull();
+ await stopping;
+ await io.runtime.start();
+ expect(io.runtime.getPacUrl()).toBe(pacUrl);
+ expect(() => assertWindowsCompatibilityContext(captured)).toThrow("desktop_compatibility_launch_owner_unverified");
+ expect(() => assertWindowsCompatibilityContext(captureWindowsCompatibilityContext([root]))).not.toThrow();
+ });
+ test("observation cancels a pending apply request before its build preflight can finish", async () => {
+ let pending = false, entered!: () => void, release!: (value: boolean) => void;
+ const waiting = new Promise(resolve => { entered = resolve; });
+ const io = fixture(true, undefined, () => pending ? (entered(), new Promise(resolve => { release = resolve; })) : true);
+ await io.runtime.start();
+ const pac = await fetch(io.runtime.getPacUrl()!).then(res => res.text()), port = /PROXY 127\.0\.0\.1:(\d+)/.exec(pac)![1];
+ await fetch("https://chatgpt.com/backend-api/wham/usage", { proxy: `http://127.0.0.1:${port}`, tls: { ca: io.authority.certPem } }).then(res => res.json());
+ pending = true; const applying = io.runtime.apply(true); await waiting; await io.runtime.observe(); release(true);
+ expect((await applying).accepted).toBe(false); expect(io.runtime.status().usage?.mode).toBe("observe");
+ });
+ test("construction/status are inert and untrusted certificates cannot start listeners", async () => {
+ let effects = 0;
+ const dormant = createDesktopCompatibilityRuntime({ loadAuthority: async () => { effects++; throw new Error(); } });
+ expect(dormant.status().running).toBe(false); expect(dormant.getPacUrl()).toBeNull(); expect(effects).toBe(0);
+ const io = fixture(false); await expect(io.runtime.start()).rejects.toThrow("trust_required");
+ expect(io.runtime.status().phase).toBe("off"); expect(desktopCompatibilityRuntimeActive()).toBe(false);
+ });
+ test("real loopback CONNECT/TLS preserves requests and restricts correction to an explicit trial", async () => {
+ const io = fixture();
+ await io.runtime.start(); expect(io.runtime.status().phase).toBe("running"); expect(io.runtime.status().usage?.mode).toBe("observe");
+ const pac = await fetch(io.runtime.getPacUrl()!).then(res => res.text());
+ expect(pac).toContain("; DIRECT"); expect(pac).toContain('host.toLowerCase() === "chatgpt.com"');
+ const port = /PROXY 127\.0\.0\.1:(\d+)/.exec(pac)![1];
+ const send = (path: string, init: RequestInit = {}) => fetch("https://chatgpt.com" + path, {
+ ...init, proxy: `http://127.0.0.1:${port}`, tls: { ca: io.authority.certPem },
+ });
+ expect(await send("/backend-api/wham/usage").then(res => res.json())).toEqual(usage);
+ const initial = await send("/backend-api/wham/usage/stream"), originalStream = initial.body!.getReader();
+ expect(new TextDecoder().decode((await originalStream.read()).value)).toContain('"allowed":false');
+ expect((await io.runtime.apply(true)).accepted).toBe(true);
+ expect((await originalStream.read()).done).toBe(true); originalStream.releaseLock(); expect(io.cancelledStreams()).toBe(1);
+ const next = await send("/backend-api/wham/usage/stream"), correctedStream = next.body!.getReader();
+ const event = new TextDecoder().decode((await correctedStream.read()).value);
+ expect(event).toContain('"sequence":2'); expect(event).toContain('"allowed":true');
+ await correctedStream.cancel(); correctedStream.releaseLock();
+ const changedResponse = await send("/backend-api/wham/usage");
+ expect(changedResponse.headers.get("etag")).toBeNull(); expect(changedResponse.headers.get("cache-control")).toBe("no-store");
+ const adjusted = await changedResponse.json();
+ expect(adjusted.rate_limit.allowed).toBe(true); expect(adjusted.rate_limit.primary_window.used_percent).toBe(100); expect(adjusted.credits.has_credits).toBe(false);
+ const bytes = new Uint8Array([0, 255, 1, 128, 42]);
+ const uploaded = await send("/backend-api/files", { method: "POST", body: bytes, headers: { cookie: "fixture=session" } });
+ expect(new Uint8Array(await uploaded.arrayBuffer())).toEqual(bytes); expect(uploaded.headers.get("set-cookie")).toContain("fixture=kept; Secure");
+ expect(io.calls.at(-1)).toEqual({ path: "/backend-api/files", method: "POST", bytes: 5, cookie: "fixture=session" });
+ await io.runtime.observe(); expect(await send("/backend-api/wham/usage").then(res => res.json())).toEqual(usage);
+ const cert = createDesktopCertificateService("unused", { platform: "win32" });
+ await expect(cert.renew("A".repeat(64))).rejects.toMatchObject({ code: "runtime_running" });
+ await io.runtime.stop(); expect(io.runtime.status().phase).toBe("off"); expect(desktopCompatibilityRuntimeActive()).toBe(false);
+ expect(await io.runtime.stop()).toMatchObject({ phase: "off" });
+ await expect(fetch(`http://127.0.0.1:${port}`, { signal: AbortSignal.timeout(1000) })).rejects.toThrow();
+ }, 15000);
+ test("core shutdown owns teardown and prevents reactivation in a draining process", async () => {
+ const io = fixture(); await io.runtime.start();
+ runOptionalShutdownHooks(); await io.runtime.stop();
+ expect(io.runtime.status().running).toBe(false);
+ await expect(io.runtime.start()).rejects.toThrow("stopping");
+ });
+ test("an unassessed app update cannot start or rearm the response correction", async () => {
+ const io = fixture(); io.setBuildSupported(false);
+ await expect(io.runtime.start()).rejects.toThrow("build_unverified"); expect(desktopCompatibilityRuntimeActive()).toBe(false);
+ io.setBuildSupported(true); await io.runtime.start(); io.setBuildSupported(false);
+ await expect(io.runtime.apply(true)).rejects.toThrow("build_unverified"); expect(io.runtime.status().usage?.mode).toBe("observe");
+ });
+ test("an active trial disarms on an app update or routing change before returning the next usage response", async () => {
+ for (const failure of ["build", "routing"] as const) {
+ const io = fixture(); await io.runtime.start();
+ const pac = await fetch(io.runtime.getPacUrl()!).then(res => res.text()), port = /PROXY 127\.0\.0\.1:(\d+)/.exec(pac)![1];
+ const read = () => fetch("https://chatgpt.com/backend-api/wham/usage", { proxy: `http://127.0.0.1:${port}`, tls: { ca: io.authority.certPem } }).then(res => res.json());
+ await read(); expect((await io.runtime.apply(true)).accepted).toBe(true); expect((await read()).rate_limit.allowed).toBe(true);
+ if (failure === "build") io.setBuildSupported(false); else io.setRoutingSupported(false);
+ expect(await read()).toEqual(usage); expect(io.runtime.status().usage?.mode).toBe("observe");
+ expect(io.runtime.status().contextFailure).toBe(failure === "build" ? "build_unverified" : "native_routing_unverified");
+ await io.runtime.stop();
+ }
+ });
+ test("routing preflight refuses a start before opening listeners", async () => {
+ const io = fixture(); io.setRoutingSupported(false);
+ await expect(io.runtime.start()).rejects.toThrow("native_routing_unverified");
+ expect(io.runtime.status().running).toBe(false); expect(io.runtime.getPacUrl()).toBeNull(); expect(desktopCompatibilityRuntimeActive()).toBe(false);
+ });
+ test("a cached PAC reconnects to the same ports after restart and Apply is never resumed", async () => {
+ const io = fixture(); await io.runtime.start();
+ const originalUrl = io.runtime.getPacUrl()!, pac = await fetch(originalUrl).then(res => res.text());
+ const port = Number(/PROXY 127\.0\.0\.1:(\d+)/.exec(pac)![1]);
+ const request = () => fetch("https://chatgpt.com/backend-api/wham/usage", { proxy: `http://127.0.0.1:${port}`, tls: { ca: io.authority.certPem } }).then(res => res.json());
+ expect((await request()).rate_limit.allowed).toBe(false); await io.runtime.apply(true);
+ expect((await request()).rate_limit.allowed).toBe(true);
+ await io.runtime.stop(); await io.runtime.start();
+ expect(io.runtime.getPacUrl()).toBe(originalUrl); expect(await fetch(originalUrl).then(res => res.text())).toBe(pac);
+ expect((await request()).rate_limit.allowed).toBe(false); expect(io.runtime.status().usage?.mode).toBe("observe");
+ });
+ test("a reused port conflict refuses startup and never replaces the cached connection identity", async () => {
+ let stored: DesktopConnectionIdentity | null = null;
+ const io = fixture(true, { read: () => stored, publish: async value => (stored ??= value) });
+ await io.runtime.start(); const originalUrl = io.runtime.getPacUrl(); await io.runtime.stop();
+ const original = { ...stored! };
+ for (const occupied of ["connectPort", "pacPort"] as const) {
+ const blocker = createTcpServer();
+ await new Promise(resolve => blocker.listen(original[occupied], "127.0.0.1", resolve));
+ try {
+ await expect(io.runtime.start()).rejects.toThrow("connection_unavailable");
+ expect(stored).toEqual(original); expect(io.runtime.status().phase).toBe("off"); expect(desktopCompatibilityRuntimeActive()).toBe(false);
+ // The other port must be bindable even when startup had already opened it.
+ const probe = createTcpServer();
+ try { await new Promise((resolve, reject) => { probe.once("error", reject); probe.listen(original[occupied === "connectPort" ? "pacPort" : "connectPort"], "127.0.0.1", resolve); }); }
+ finally { await new Promise(resolve => probe.close(() => resolve())); }
+ } finally { await new Promise(resolve => blocker.close(() => resolve())); }
+ await io.runtime.start(); expect(io.runtime.getPacUrl()).toBe(originalUrl); await io.runtime.stop();
+ }
+ });
+});
+
+describe("compatibility trials consume their exhausted observation", () => {
+ for (const ending of ["cancel", "timeout"] as const) {
+ for (const observeDuringTrial of [false, true]) {
+ test(`${ending} needs a new observation (observed during trial: ${observeDuringTrial})`, async () => {
+ let now = 1000;
+ const activation = new UsageActivation("account", async () => ({ requested: 0, closed: 0, failed: 0 }),
+ async () => "account", () => now);
+ activation.observe("account", "exhausted");
+ expect((await activation.activate(consent)).accepted).toBe(true);
+ expect((await activation.activate(consent)).reason).toBe("activation-already-pending");
+ const oldGeneration = activation.snapshot().generation;
+ if (observeDuringTrial) activation.observe("account", "exhausted");
+ if (ending === "cancel") await activation.observeOnly();
+ else { now += 180000; expect(await activation.expireIfNeeded()).toBe(true); }
+ expect(activation.snapshot().mode).toBe("observe");
+ expect(activation.recordOutput("account", oldGeneration)).toBe(false);
+ // Still inside the old observation's five-minute TTL: consent is not enough.
+ expect(await activation.activate(consent)).toMatchObject({ accepted: false, reason: "no-fresh-eligible-exhaustion" });
+ expect(activation.observe("another-account", "exhausted")).toBe(false);
+ expect((await activation.activate(consent)).accepted).toBe(false);
+ activation.observe("account", "exhausted");
+ expect((await activation.activate({ ...consent, accountWideConsent: false })).accepted).toBe(false);
+ expect((await activation.activate(consent)).accepted).toBe(true);
+ });
+ }
+ }
+ for (const failure of ["throw", "partial", "invalid-count"] as const) {
+ test(`${failure} clears observations and outputs produced while refresh was pending`, async () => {
+ let fail = true;
+ const activation = new UsageActivation("account", async () => {
+ if (!fail) return { requested: 0, closed: 0, failed: 0 };
+ activation.observe("account", "exhausted");
+ activation.recordOutput("account", activation.snapshot().generation);
+ if (failure === "throw") throw new Error("synthetic refresh failure");
+ return failure === "partial" ? { requested: 1, closed: 0, failed: 1 }
+ : { requested: Number.NaN, closed: 0, failed: 0 };
+ }, async () => "account", () => 1000);
+ activation.observe("account", "exhausted");
+ expect(await activation.activate(consent)).toMatchObject({ accepted: false, reason: "refresh-failed", mode: "observe", outputs: 0 });
+ fail = false;
+ expect(await activation.activate(consent)).toMatchObject({ accepted: false, reason: "no-fresh-eligible-exhaustion" });
+ activation.observe("account", "exhausted");
+ expect((await activation.activate(consent)).accepted).toBe(true);
+ });
+ }
+ test("a superseded refresh failure cannot erase a newer trial", async () => {
+ let calls = 0, entered!: () => void, rejectOld!: (error: Error) => void;
+ const enteredPromise = new Promise(resolve => { entered = resolve; });
+ const activation = new UsageActivation("account", async () => {
+ if (++calls === 1) {
+ entered();
+ return new Promise<{ requested: number; closed: number; failed: number }>((_, reject) => { rejectOld = reject; });
+ }
+ return { requested: 0, closed: 0, failed: 0 };
+ }, async () => "account", () => 1000);
+ activation.observe("account", "exhausted");
+ const old = activation.activate(consent); await enteredPromise;
+ await activation.observeOnly();
+ activation.observe("account", "exhausted");
+ expect((await activation.activate(consent)).accepted).toBe(true);
+ const generation = activation.snapshot().generation;
+ rejectOld(new Error("superseded refresh"));
+ expect((await old).accepted).toBe(false);
+ expect(activation.snapshot()).toMatchObject({ mode: "apply", generation });
+ expect(activation.recordOutput("account", generation)).toBe(true);
+ });
+});
diff --git a/tests/clients/desktop-compatibility-trust.test.ts b/tests/clients/desktop-compatibility-trust.test.ts
new file mode 100644
index 00000000000..db580c72246
--- /dev/null
+++ b/tests/clients/desktop-compatibility-trust.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, test } from "bun:test";
+import { randomUUID, X509Certificate } from "node:crypto";
+import { createCertificateAuthority } from "../../src/claude/intercept/local-ca";
+import type { StoredDesktopAuthority } from "../../src/codex/desktop-compatibility/certificate-store";
+import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust, type TrustOperation } from "../../src/codex/desktop-compatibility/windows-certificate-trust";
+
+function fixture(serverAuthOnly = true): StoredDesktopAuthority {
+ const commonName = `OpenCodex Codex Desktop ${randomUUID()}`;
+ const authority = createCertificateAuthority({ commonName, validityDays: 1, permittedDnsNames: ["chatgpt.com"], excludeAllIpAddresses: true, serverAuthOnly });
+ const certificate = new X509Certificate(authority.certPem);
+ return { authority, commonName, fingerprint: certificate.fingerprint256.replaceAll(":", ""), expiresAt: Date.parse(certificate.validTo), renewalDue: true, reused: false };
+}
+
+describe("exact Windows compatibility certificate trust", () => {
+ test("legacy purpose refuses trust before an OS write but allows exact removal", async () => {
+ const value = fixture(false), calls: TrustOperation[] = []; let state: DesktopCertificateTrust = "trusted";
+ const controller = createWindowsCertificateTrust(value, value.fingerprint, async operation => {
+ calls.push(operation); if (operation === "remove") state = "not-trusted"; return state;
+ });
+ await expect(controller.trust()).rejects.toThrow("desktop_compatibility_authority_renewal_required");
+ expect(calls).toHaveLength(0);
+ expect(await controller.remove()).toBe("not-trusted");
+ expect(calls).toEqual(["inspect", "remove", "inspect"]);
+ });
+
+ test("repeated trust and removal do not repeat OS mutations", async () => {
+ const value = fixture(), calls: TrustOperation[] = []; let state: DesktopCertificateTrust = "not-trusted";
+ const controller = createWindowsCertificateTrust(value, value.fingerprint, async (operation, publicDer, fingerprint) => {
+ calls.push(operation); expect(fingerprint).toBe(value.fingerprint);
+ expect(Buffer.from(publicDer, "base64")).toEqual(new X509Certificate(value.authority.certPem).raw);
+ if (operation === "trust") state = "trusted";
+ if (operation === "remove") state = "not-trusted";
+ return state;
+ });
+ expect(await controller.trust()).toBe("trusted"); expect(await controller.trust()).toBe("trusted");
+ expect(await controller.remove()).toBe("not-trusted"); expect(await controller.remove()).toBe("not-trusted");
+ expect(calls.filter(call => call === "trust")).toHaveLength(1);
+ expect(calls.filter(call => call === "remove")).toHaveLength(1);
+ });
+
+ test("an uncertain command result is settled by store readback, not a second mutation", async () => {
+ const value = fixture(), calls: TrustOperation[] = []; let state: DesktopCertificateTrust = "not-trusted";
+ const controller = createWindowsCertificateTrust(value, value.fingerprint, async operation => {
+ calls.push(operation);
+ if (operation === "trust") { state = "trusted"; throw new Error("lost acknowledgement"); }
+ return state;
+ });
+ expect(await controller.trust()).toBe("trusted");
+ expect(calls).toEqual(["inspect", "trust", "inspect"]);
+ });
+
+ test("unknown trust state never authorizes a write", async () => {
+ const value = fixture(), calls: TrustOperation[] = [];
+ const controller = createWindowsCertificateTrust(value, value.fingerprint, async operation => { calls.push(operation); return "unknown"; });
+ expect(await controller.trust()).toBe("unknown"); expect(await controller.remove()).toBe("unknown");
+ expect(calls).toEqual(["inspect", "inspect"]);
+ });
+
+ test("a stale fingerprint or an unrelated private key is refused before any OS request", () => {
+ const value = fixture(), other = fixture();
+ expect(() => createWindowsCertificateTrust(value, other.fingerprint)).toThrow("desktop_compatibility_certificate_mismatch");
+ expect(() => createWindowsCertificateTrust({ ...value, authority: { ...value.authority, privateKey: other.authority.privateKey } }, value.fingerprint))
+ .toThrow("desktop_compatibility_certificate_mismatch");
+ });
+
+ test("concurrent mutations are not queued into duplicate certificate prompts", async () => {
+ const value = fixture(); let release!: (state: DesktopCertificateTrust) => void;
+ let reads = 0;
+ const controller = createWindowsCertificateTrust(value, value.fingerprint, async () => ++reads === 1
+ ? await new Promise(resolve => { release = resolve; }) : "trusted");
+ const first = controller.trust();
+ await expect(controller.remove()).rejects.toThrow("desktop_compatibility_trust_busy");
+ release("trusted"); expect(await first).toBe("trusted");
+ });
+
+ test.skipIf(process.platform !== "win32")("real CurrentUser Root inspection remains read-only for an unregistered synthetic CA", async () => {
+ const value = fixture();
+ expect(await inspectWindowsCertificateTrust(value.authority.certPem, value.fingerprint)).toBe("not-trusted");
+ }, 15_000);
+});
diff --git a/tests/fixtures/test-layout-expected-additional.json b/tests/fixtures/test-layout-expected-additional.json
new file mode 100644
index 00000000000..844b1071928
--- /dev/null
+++ b/tests/fixtures/test-layout-expected-additional.json
@@ -0,0 +1,40 @@
+{
+ "provider-antigravity-wire-snapshot.test.ts": "providers",
+ "antigravity-discovered-families.test.ts": "adapters/google",
+ "provider-antigravity-effort-families.test.ts": "providers",
+ "provider-antigravity-family-catalog.test.ts": "providers",
+ "claude-subagent-model-force.test.ts": "claude-integration",
+ "subagent-model-force-api.test.ts": "server",
+ "agent-subagent-force.test.ts": "cli",
+ "compaction-event-ownership.test.ts": "responses",
+ "zed-retained-budget.test.ts": "providers",
+ "jev-decision-scope.test.ts": "server",
+ "codex-shim-path-readiness.test.ts": "codex-integration",
+ "codex-shim-fnm.test.ts": "codex-integration",
+ "claude-intercept-cleanup.test.ts": "claude-integration",
+ "droid-managed-reasoning-defaults.test.ts": "clients",
+ "management-droid-reasoning-defaults.test.ts": "server",
+ "droid-reasoning-defaults.test.ts": "responses",
+ "minimax-combo-image-input.test.ts": "providers",
+ "web-search-forced-declaration.test.ts": "web-search",
+ "responses-hosted-image-limits.test.ts": "responses",
+ "antigravity-refusal.test.ts": "adapters/google",
+ "catalog-custom-ordering.test.ts": "codex-integration",
+ "claude-first-party-agent-sync.test.ts": "claude-integration",
+ "cli-account-verify-status.test.ts": "cli",
+ "opencode-kilo-refresh.test.ts": "clients",
+ "client-export-reasoning-metadata.test.ts": "clients",
+ "client-export-reasoning-controls.test.ts": "clients",
+ "client-export-live-wire.test.ts": "clients",
+ "client-export-effective-metadata.test.ts": "clients",
+ "grok-session-identity.test.ts": "providers/xai",
+ "responses-forward-client-headers.test.ts": "responses",
+ "opengateway-provider.test.ts": "providers",
+ "opengateway-key-validation.test.ts": "providers",
+ "request-log-generation-window.test.ts": "usage",
+ "responses-chatgpt-web-replay-metadata.test.ts": "responses",
+ "server-antigravity-verify-replay-scope.test.ts": "server",
+ "sse-rewrite-line-endings.test.ts": "responses",
+ "chat-sse-framing-guard.test.ts": "responses",
+ "usage-throughput.test.ts": "usage"
+}
diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json
index c74522309a9..b1fc83e828f 100644
--- a/tests/fixtures/test-layout-expected.json
+++ b/tests/fixtures/test-layout-expected.json
@@ -1,26 +1,11 @@
{
- "usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers",
- "antigravity-discovered-families.test.ts": "adapters/google",
- "provider-antigravity-effort-families.test.ts": "providers",
- "provider-antigravity-family-catalog.test.ts": "providers",
- "claude-subagent-model-force.test.ts": "claude-integration",
- "subagent-model-force-api.test.ts": "server",
+ "usage-antigravity-55.test.ts": "usage",
"caller-session-identity.test.ts": "server",
"caller-session-ingress.test.ts": "server",
- "agent-subagent-force.test.ts": "cli",
- "compaction-event-ownership.test.ts": "responses",
- "zed-retained-budget.test.ts": "providers",
- "jev-decision-scope.test.ts": "server",
"server-combo-plan-model-refusal.test.ts": "server",
"codex-main-token-invalidated.test.ts": "codex-integration",
"codex-main-grant-refusal.test.ts": "codex-integration",
"responses-alternate-main-cancellation.test.ts": "responses",
- "codex-shim-path-readiness.test.ts": "codex-integration",
- "codex-shim-fnm.test.ts": "codex-integration",
- "claude-intercept-cleanup.test.ts": "claude-integration",
- "droid-managed-reasoning-defaults.test.ts": "clients",
- "management-droid-reasoning-defaults.test.ts": "server",
- "droid-reasoning-defaults.test.ts": "responses",
"claude-intercept-on-demand.test.ts": "claude-integration",
"main-account-hard-lock-thresholds.test.ts": "codex-integration",
"main-account-external-usage.test.ts": "codex-integration",
@@ -30,24 +15,21 @@
"desktop-app-server-shim-launcher.test.ts": "clients",
"desktop-chatgpt-config.test.ts": "clients",
"owner-registry-acl.test.ts": "config",
- "minimax-combo-image-input.test.ts": "providers",
- "web-search-forced-declaration.test.ts": "web-search",
"claude-devin-output-order.test.ts": "claude-integration",
"codex-config-preservation.test.ts": "codex-integration",
- "codex-credits.test.ts": "codex-integration",
- "codex-credits-settings.test.ts": "codex-integration",
- "codex-credits-probes.test.ts": "codex-integration",
- "codex-credits-after-limit.test.ts": "codex-integration",
- "codex-credits-after-limit-main.test.ts": "codex-integration",
- "service-desktop-startup-health.test.ts": "service",
- "service-desktop-startup.test.ts": "service",
- "startup-health-packaged-probe.test.ts": "server",
+ "codex-credits.test.ts": "codex-integration",
+ "codex-credits-settings.test.ts": "codex-integration",
+ "codex-credits-probes.test.ts": "codex-integration",
+ "codex-credits-after-limit.test.ts": "codex-integration",
+ "codex-credits-after-limit-main.test.ts": "codex-integration",
+ "service-desktop-startup-health.test.ts": "service",
+ "service-desktop-startup.test.ts": "service",
+ "startup-health-packaged-probe.test.ts": "server",
"discovered-native-models.test.ts": "codex-integration",
- "responses-hosted-image-delivery.test.ts": "responses",
- "responses-hosted-image-display.test.ts": "responses",
- "responses-hosted-image-limits.test.ts": "responses",
+ "responses-hosted-image-delivery.test.ts": "responses",
+ "responses-hosted-image-display.test.ts": "responses",
"cursor-local-installer.test.ts": "providers/cursor",
- "codex-quota-query-backoff.test.ts": "codex-integration",
+ "codex-quota-query-backoff.test.ts": "codex-integration",
"link-relay-bound-transport.test.ts": "clients",
"pnpm-command-isolation.test.ts": "update",
"project-config-warning-snapshot.test.ts": "codex-integration",
@@ -133,7 +115,7 @@
"anthropic-combo-account-cooldown.test.ts": "adapters/anthropic",
"cli-alias-json.test.ts": "cli",
"cli-anthropic-account-threshold.test.ts": "cli",
- "anthropic-model-routes.test.ts": "adapters/anthropic",
+ "anthropic-model-routes.test.ts": "adapters/anthropic",
"anthropic-agentrouter-language-framing.test.ts": "adapters/anthropic",
"anthropic-baseurl-override.test.ts": "adapters/anthropic",
"anthropic-compatible-stream.test.ts": "adapters/anthropic",
@@ -172,7 +154,6 @@
"anthropic-tool-declaration-constraints.test.ts": "adapters/anthropic",
"anthropic-tool-schema.test.ts": "adapters/anthropic",
"antigravity-baseurl-override.test.ts": "adapters/google",
- "antigravity-refusal.test.ts": "adapters/google",
"antigravity-static-catalog.test.ts": "adapters/google",
"api-access-endpoints.test.ts": "server",
"api-catalog-route.test.ts": "server",
@@ -239,7 +220,6 @@
"cancel-body-on-abort.test.ts": "server",
"catalog-auto-refresh-scheduler.test.ts": "codex-integration",
"catalog-cursor-search.test.ts": "codex-integration",
- "catalog-custom-ordering.test.ts": "codex-integration",
"catalog-duplicate-slug-dedup.test.ts": "codex-integration",
"catalog-free-pricing-status.test.ts": "codex-integration",
"catalog-full-picker-order.test.ts": "codex-integration",
@@ -355,7 +335,6 @@
"claude-desktop-remote-hub.test.ts": "claude-integration",
"claude-dotenv-provenance-transport.test.ts": "claude-integration",
"claude-estimate-projection.test.ts": "claude-integration",
- "claude-first-party-agent-sync.test.ts": "claude-integration",
"claude-first-party-union.test.ts": "claude-integration",
"claude-desktop-system-proxy.test.ts": "claude-integration",
"claude-gateway-cache.test.ts": "claude-integration",
@@ -398,7 +377,6 @@
"cli-account-pin-drain.test.ts": "cli",
"cli-account-pool-verbs.test.ts": "cli",
"cli-account-threshold.test.ts": "cli",
- "cli-account-verify-status.test.ts": "cli",
"cli-account.test.ts": "cli",
"cli-capabilities-arguments.test.ts": "cli",
"cli-capabilities.test.ts": "cli",
@@ -433,7 +411,8 @@
"cli-models-free-only.test.ts": "cli",
"cli-models-price.test.ts": "cli",
"cli-models-reasoning.test.ts": "cli",
- "cli-models-runtime-dispatch.test.ts": "cli", "cli-models-set.test.ts": "cli",
+ "cli-models-runtime-dispatch.test.ts": "cli",
+ "cli-models-set.test.ts": "cli",
"cli-models.test.ts": "cli",
"cli-native-profile.test.ts": "cli",
"cli-observe-logs.test.ts": "cli",
@@ -462,11 +441,6 @@
"client-catalog-compatibility.test.ts": "clients",
"client-config-export-new-clients.test.ts": "config",
"client-config-export.test.ts": "config",
- "opencode-kilo-refresh.test.ts": "clients",
- "client-export-reasoning-metadata.test.ts": "clients",
- "client-export-reasoning-controls.test.ts": "clients",
- "client-export-live-wire.test.ts": "clients",
- "client-export-effective-metadata.test.ts": "clients",
"client-config-new-clients.test.ts": "config",
"client-connect.test.ts": "clients",
"client-export-modality-enum.test.ts": "clients",
@@ -601,7 +575,7 @@
"codex-pool-request-owned-main.test.ts": "codex-integration",
"codex-pool-rotation.test.ts": "codex-integration",
"codex-priority-failback.test.ts": "codex-integration",
- "codex-idle-window.test.ts": "codex-integration",
+ "codex-idle-window.test.ts": "codex-integration",
"codex-prompt-adopt.test.ts": "codex-integration",
"codex-prompt-base-variants.test.ts": "codex-integration",
"codex-prompt-journal.test.ts": "codex-integration",
@@ -943,7 +917,6 @@
"grok-reset-coupon-cli.test.ts": "providers/xai",
"grok-reset-coupons.test.ts": "providers/xai",
"grok-selection.test.ts": "providers/xai",
- "grok-session-identity.test.ts": "providers/xai",
"grok-status.test.ts": "providers/xai",
"grok-sync.test.ts": "providers/xai",
"grok-writer-boundary.test.ts": "providers/xai",
@@ -1182,7 +1155,8 @@
"model-presets.test.ts": "providers",
"model-rename-migration.test.ts": "providers",
"model-roster-seed-repair.test.ts": "providers",
- "model-selection-guidance.test.ts": "cli", "model-settings-management-api.test.ts": "server",
+ "model-selection-guidance.test.ts": "cli",
+ "model-settings-management-api.test.ts": "server",
"model-visibility-management-api.test.ts": "codex-integration",
"models-feedback-callback.test.ts": "gui",
"models-free-filter.test.ts": "gui",
@@ -1300,7 +1274,6 @@
"openai-provider-option-startup.test.ts": "adapters/openai",
"openai-provider-option-tooling.test.ts": "adapters/openai",
"openai-provider-option.test.ts": "adapters/openai",
- "responses-forward-client-headers.test.ts": "responses",
"openai-responses-passthrough.test.ts": "responses",
"openai-responses-summary-none.test.ts": "responses",
"responses-forward-output-cap.test.ts": "responses",
@@ -1316,8 +1289,6 @@
"opencode-management-transport.test.ts": "providers",
"opencode-zen-deepseek-reasoning.test.ts": "providers",
"opencode-zen-rate-limit.test.ts": "providers",
- "opengateway-provider.test.ts": "providers",
- "opengateway-key-validation.test.ts": "providers",
"openrouter-provider-routing.test.ts": "providers",
"openrouter-quota-reset-cooldown-4024.test.ts": "providers",
"opper-provider.test.ts": "providers",
@@ -1479,7 +1450,6 @@
"request-log-nonstream.test.ts": "usage",
"request-log-protocol-trace.test.ts": "usage",
"request-log-served-model.test.ts": "usage",
- "request-log-generation-window.test.ts": "usage",
"request-log.test.ts": "usage",
"request-outcome-agreement.test.ts": "usage",
"request-pacing.test.ts": "usage",
@@ -1505,7 +1475,6 @@
"responses-azure-opaque-blob-recovery.test.ts": "responses",
"responses-bare-echo-helper-fence.test.ts": "responses",
"responses-canonical-only-top-level-fields.test.ts": "responses",
- "responses-chatgpt-web-replay-metadata.test.ts": "responses",
"responses-code-mode-goal-helpers.test.ts": "responses",
"responses-code-mode-mcp-direct.test.ts": "responses",
"local-read-response-proof.test.ts": "server",
@@ -1617,7 +1586,6 @@
"self-launch-argv.test.ts": "lib",
"server-403-permission-e2e.test.ts": "server",
"server-agent-task-recovery-replay.test.ts": "server",
- "server-antigravity-verify-replay-scope.test.ts": "server",
"server-auth-localhost-bind.test.ts": "server",
"server-auth-scoped-quota.test.ts": "server",
"server-auth.test.ts": "server",
@@ -1714,8 +1682,6 @@
"sse-inspector-bounds.test.ts": "responses",
"sse-null-data-frame.test.ts": "responses",
"sse-payload-rewrite.test.ts": "responses",
- "sse-rewrite-line-endings.test.ts": "responses",
- "chat-sse-framing-guard.test.ts": "responses",
"sse-unspaced-data-fields.test.ts": "responses",
"stale-state-purge.test.ts": "service",
"stall-subprocess-exit.test.ts": "lib",
@@ -1843,7 +1809,6 @@
"usage-shape-extraction.test.ts": "usage",
"usage-spend-cache-provenance.test.ts": "usage",
"usage-summary.test.ts": "usage",
- "usage-throughput.test.ts": "usage",
"usage-surfaces.test.ts": "usage",
"usage-time-range.test.ts": "usage",
"usage-timeline.test.ts": "usage",
@@ -1974,5 +1939,20 @@
"injection-routing-healer.test.ts": "codex-integration",
"injection-routing-heal-apply.test.ts": "codex-integration",
"cli-start-routing-heal-wiring.test.ts": "cli",
- "cli-status-codex-routing-drift.test.ts": "cli"
+ "cli-status-codex-routing-drift.test.ts": "cli",
+ "desktop-compatibility-authority.test.ts": "clients",
+ "desktop-compatibility-native-identity.test.ts": "clients",
+ "desktop-compatibility-build-probe.test.ts": "clients",
+ "desktop-compatibility-connection-store.test.ts": "clients",
+ "desktop-compatibility-routing.test.ts": "clients",
+ "optional-desktop-upstream.test.ts": "lib",
+ "server-desktop-compatibility-startup.test.ts": "server",
+ "management-desktop-compatibility-settings.test.ts": "server",
+ "desktop-compatibility-runtime.test.ts": "clients",
+ "desktop-compatibility-relay.test.ts": "clients",
+ "management-desktop-compatibility-runtime-routes.test.ts": "server",
+ "desktop-compatibility-certificate-service.test.ts": "clients",
+ "desktop-compatibility-launch.test.ts": "clients",
+ "management-desktop-compatibility-routes.test.ts": "server",
+ "desktop-compatibility-trust.test.ts": "clients"
}
diff --git a/tests/helpers/desktop-egress-fixture.ts b/tests/helpers/desktop-egress-fixture.ts
new file mode 100644
index 00000000000..fd2e60d083c
--- /dev/null
+++ b/tests/helpers/desktop-egress-fixture.ts
@@ -0,0 +1,52 @@
+import { createServer as createHttp } from "node:http";
+import { createServer as createHttps } from "node:https";
+import { createServer as createTcp, connect, type Socket, type Server } from "node:net";
+import type { PemKeyPair } from "../../src/claude/intercept/local-ca";
+export async function listenFixture(server: Server, host = "127.0.0.1") {
+ const sockets = new Set();
+ server.on("connection", socket => { sockets.add(socket); socket.on("error", () => {}); socket.once("close", () => sockets.delete(socket)); });
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, host, resolve); });
+ return { port: (server.address() as { port: number }).port,
+ async close() { for (const socket of sockets) socket.destroy(); await new Promise(resolve => server.close(() => resolve())); } };
+}
+export async function forwardProxy(kind: "http" | "https" | "socks5", targetPort: number, leaf: PemKeyPair) {
+ const sockets = new Set(), seen: { authority: string; auth: string }[] = [];
+ const pipe = (client: Socket, head = Buffer.alloc(0)) => {
+ const target = connect({ host: "127.0.0.1", port: targetPort }); sockets.add(target);
+ target.on("error", () => client.destroy()); client.on("error", () => target.destroy());
+ target.on("close", () => { sockets.delete(target); client.destroy(); }); client.on("close", () => target.destroy());
+ target.once("connect", () => { if (head.length) target.write(head); client.pipe(target).pipe(client); });
+ };
+ const server = kind === "socks5" ? createTcp(client => {
+ let held = Buffer.alloc(0), stage = 0, auth = "";
+ const onData = (chunk: Buffer) => {
+ held = Buffer.concat([held, chunk]);
+ for (;;) {
+ if (stage === 0) {
+ if (held.length < 2 || held.length < 2 + held[1]!) return;
+ const end = 2 + held[1]!; const supportsAuth = held.subarray(2, end).includes(2);
+ held = held.subarray(end); client.write(Buffer.from([5, supportsAuth ? 2 : 255])); stage = 1;
+ } else if (stage === 1) {
+ if (held.length < 2 || held.length < 3 + held[1]!) return;
+ const n = held[1]!, m = held[2 + n]!; if (held.length < 3 + n + m) return;
+ auth = held.subarray(2, 2 + n).toString() + ":" + held.subarray(3 + n, 3 + n + m).toString();
+ held = held.subarray(3 + n + m); client.write(Buffer.from([1, auth === "fixture:secret" ? 0 : 1])); stage = 2;
+ } else {
+ if (held.length < 5 || held.length < 7 + held[4]!) return;
+ const n = held[4]!, authority = held.subarray(5, 5 + n).toString() + ":" + held.readUInt16BE(5 + n);
+ seen.push({ authority, auth }); held = held.subarray(7 + n); client.off("data", onData);
+ client.write(Buffer.from([5, 0, 0, 1, 127, 0, 0, 1, 0, 0])); pipe(client, held); return;
+ }
+ }
+ };
+ client.on("data", onData);
+ }) : kind === "https" ? createHttps({ cert: leaf.certPem, key: leaf.keyPem }) : createHttp();
+ if (kind !== "socks5") server.on("connect", (req, client, head) => {
+ const auth = String(req.headers["proxy-authorization"] ?? ""); seen.push({ authority: req.url ?? "", auth });
+ if (auth !== "Basic " + Buffer.from("fixture:secret").toString("base64")) { client.end("HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n"); return; }
+ client.write("HTTP/1.1 200 Connection Established\r\n\r\n"); pipe(client as Socket, head);
+ });
+ const listening = await listenFixture(server, kind === "https" ? "localhost" : "127.0.0.1");
+ return { ...listening, seen, url: `${kind}://fixture:secret@${kind === "https" ? "localhost" : "127.0.0.1"}:${listening.port}`,
+ async close() { for (const socket of sockets) socket.destroy(); await listening.close(); } };
+}
diff --git a/tests/helpers/desktop-egress-worker.ts b/tests/helpers/desktop-egress-worker.ts
new file mode 100644
index 00000000000..44ef9d1e579
--- /dev/null
+++ b/tests/helpers/desktop-egress-worker.ts
@@ -0,0 +1,5 @@
+import { desktopOutboundFetch } from "../../src/lib/desktop-proxy-route";
+const response = await desktopOutboundFetch("https://chatgpt.com/fixture", {
+ method: "POST", body: new Uint8Array([0, 255, 42]), signal: AbortSignal.timeout(5000), headers: { cookie: "fixture=session" },
+});
+console.log(JSON.stringify({ status: response.status, body: await response.text() }));
diff --git a/tests/lib/optional-desktop-upstream.test.ts b/tests/lib/optional-desktop-upstream.test.ts
new file mode 100644
index 00000000000..19d181b1649
--- /dev/null
+++ b/tests/lib/optional-desktop-upstream.test.ts
@@ -0,0 +1,118 @@
+import { expect, test } from "bun:test";
+import { createServer as createHttps } from "node:https";
+import { createServer as createTcp } from "node:net";
+import { dialDesktopUpstream } from "../../src/lib/desktop-upstream-tunnel";
+import { desktopProxyFor } from "../../src/lib/desktop-proxy-route";
+import { createCertificateAuthority, issueServerLeaf } from "../../src/claude/intercept/local-ca";
+import { forwardProxy, listenFixture } from "../helpers/desktop-egress-fixture";
+import { mkdtempSync, writeFileSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { repoPath, repoRoot } from "../helpers/repo-root";
+import { removeTreeWithRetry } from "../helpers/remove-tree";
+
+test("desktop HTTP and WS choose explicit NO_PROXY, supported proxies, or a closed failure", () => {
+ const url = new URL("https://chatgpt.com/");
+ expect(desktopProxyFor(url, {})).toBe(false);
+ expect(desktopProxyFor(url, { HTTPS_PROXY: "http://127.0.0.1:4444" })).toBe("http://127.0.0.1:4444");
+ expect(desktopProxyFor(url, { ALL_PROXY: "http://127.0.0.1:4444" })).toBe("http://127.0.0.1:4444");
+ expect(desktopProxyFor(url, { all_proxy: "https://127.0.0.1:4444" })).toBe("https://127.0.0.1:4444");
+ expect(desktopProxyFor(url, { HTTPS_PROXY: "http://127.0.0.1:4444", ALL_PROXY: "http://127.0.0.1:5555" })).toBe("http://127.0.0.1:4444");
+ expect(() => desktopProxyFor(url, { HTTPS_PROXY: "bad", ALL_PROXY: "http://127.0.0.1:5555" })).toThrow("proxy_invalid");
+ expect(desktopProxyFor(url, { HTTPS_PROXY: "http://127.0.0.1:4444", NO_PROXY: "chatgpt.com" })).toBe(false);
+ expect(desktopProxyFor(url, { HTTPS_PROXY: "http://127.0.0.1:4444", ALL_PROXY: "socks5://127.0.0.1:5555" })).toBe("socks5://127.0.0.1:5555");
+ expect(() => desktopProxyFor(url, { HTTPS_PROXY: "ftp://127.0.0.1:4444" })).toThrow("proxy_invalid");
+ expect(() => desktopProxyFor(url, { HTTPS_PROXY: "http://127.0.0.1:4444/path" })).toThrow("proxy_invalid");
+});
+
+for (const kind of ["http", "https", "socks5"] as const) test(`verified TLS through authenticated ${kind} proxy preserves raw request and response`, async () => {
+ const ca = createCertificateAuthority({ commonName: "egress-fixture", validityDays: 1 });
+ const leaf = issueServerLeaf(ca, "egress-fixture", ["chatgpt.com"]), proxyLeaf = issueServerLeaf(ca, "egress-fixture", ["localhost"]);
+ let cookie: string | undefined, leaked: string | undefined;
+ const upstream = createHttps({ cert: leaf.certPem, key: leaf.keyPem }, (req, res) => {
+ cookie = req.headers.cookie; leaked = req.headers["proxy-authorization"] as string | undefined;
+ res.end("fixture-response");
+ });
+ const origin = await listenFixture(upstream), proxy = await forwardProxy(kind, origin.port, proxyLeaf);
+ let socket: Awaited> | undefined;
+ try {
+ socket = await dialDesktopUpstream({ proxy: proxy.url, ca: ca.certPem, proxyCa: ca.certPem, target: { host: "chatgpt.com", port: 443 } });
+ const response = new Promise((resolve, reject) => {
+ let text = ""; socket!.on("data", bytes => { text += bytes.toString(); }); socket!.once("end", () => resolve(text)); socket!.once("error", reject);
+ });
+ socket.write("GET /fixture HTTP/1.1\r\nHost: chatgpt.com\r\nCookie: fixture=session\r\nConnection: close\r\n\r\n");
+ expect(await response).toContain("fixture-response"); expect(cookie).toBe("fixture=session"); expect(leaked).toBeUndefined();
+ expect(proxy.seen).toHaveLength(1); expect(proxy.seen[0]!.authority).toBe("chatgpt.com:443");
+ expect(proxy.seen[0]!.auth).toBe(kind === "socks5" ? "fixture:secret" : "Basic " + Buffer.from("fixture:secret").toString("base64"));
+ } finally { socket?.destroy(); await proxy.close(); await origin.close(); }
+}, 15000);
+
+for (const kind of ["http", "https", "socks5", "http-all", "https-all"] as const) test(`desktop HTTP fetch uses the same authenticated ${kind} egress and validates upstream TLS`, async () => {
+ const ca = createCertificateAuthority({ commonName: "fetch-egress-fixture", validityDays: 1 });
+ const leaf = issueServerLeaf(ca, "fetch-egress-fixture", ["chatgpt.com"]), proxyLeaf = issueServerLeaf(ca, "fetch-egress-fixture", ["localhost"]);
+ let actual: Buffer | undefined;
+ const upstream = createHttps({ cert: leaf.certPem, key: leaf.keyPem }, (req, res) => {
+ const chunks: Buffer[] = []; req.on("data", chunk => chunks.push(chunk)); req.on("end", () => {
+ actual = Buffer.concat(chunks); res.end("fixture-fetch-response");
+ });
+ });
+ const transport = kind === "http-all" ? "http" : kind === "https-all" ? "https" : kind;
+ const origin = await listenFixture(upstream), proxy = await forwardProxy(transport, origin.port, proxyLeaf);
+ const root = mkdtempSync(join(tmpdir(), "ocx-desktop-fetch-")), certificate = join(root, "fixture-ca.pem");
+ writeFileSync(certificate, ca.certPem);
+ const child = Bun.spawn([process.execPath, repoPath("tests/helpers/desktop-egress-worker.ts")], {
+ cwd: repoRoot(), stdout: "pipe", stderr: "pipe",
+ env: { ...process.env, NODE_EXTRA_CA_CERTS: certificate, HTTPS_PROXY: kind.endsWith("-all") ? "" : proxy.url, https_proxy: "", HTTP_PROXY: "", http_proxy: "", ALL_PROXY: kind.endsWith("-all") ? proxy.url : "", all_proxy: "", NO_PROXY: "", no_proxy: "" },
+ });
+ const timer = setTimeout(() => child.kill(), 10000);
+ try {
+ const [exitCode, output, error] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
+ expect({ exitCode, error }).toEqual({ exitCode: 0, error: "" });
+ expect(JSON.parse(output)).toEqual({ status: 200, body: "fixture-fetch-response" });
+ expect(actual).toEqual(Buffer.from([0, 255, 42])); expect(proxy.seen).toHaveLength(1);
+ } finally { clearTimeout(timer); if (child.exitCode === null) { child.kill(); await child.exited; } await proxy.close(); await origin.close(); removeTreeWithRetry(root); }
+}, 15000);
+
+test("untrusted proxy TLS is refused without reaching the target", async () => {
+ const ca = createCertificateAuthority({ commonName: "untrusted-proxy", validityDays: 1 });
+ const leaf = issueServerLeaf(ca, "untrusted-proxy", ["localhost"]);
+ const proxy = await forwardProxy("https", 1, leaf);
+ try { await expect(dialDesktopUpstream({ proxy: proxy.url, timeoutMs: 1000 })).rejects.toThrow("desktop_upstream_connection_failed"); expect(proxy.seen).toHaveLength(0); }
+ finally { await proxy.close(); }
+});
+
+test("trusted certificates with wrong proxy or upstream hostnames are still refused", async () => {
+ const ca = createCertificateAuthority({ commonName: "wrong-host-fixture", validityDays: 1 });
+ const wrongProxy = await forwardProxy("https", 1, issueServerLeaf(ca, "wrong-host-fixture", ["chatgpt.com"]));
+ try {
+ await expect(dialDesktopUpstream({ proxy: wrongProxy.url, proxyCa: ca.certPem, timeoutMs: 1000 })).rejects.toThrow("desktop_upstream_connection_failed");
+ expect(wrongProxy.seen).toHaveLength(0);
+ } finally { await wrongProxy.close(); }
+ const wrongLeaf = issueServerLeaf(ca, "wrong-host-fixture", ["localhost"]);
+ const upstream = await listenFixture(createHttps({ cert: wrongLeaf.certPem, key: wrongLeaf.keyPem }));
+ const proxy = await forwardProxy("http", upstream.port, wrongLeaf);
+ try { await expect(dialDesktopUpstream({ proxy: proxy.url, ca: ca.certPem, timeoutMs: 1000 })).rejects.toThrow("desktop_upstream_connection_failed"); }
+ finally { await proxy.close(); await upstream.close(); }
+});
+
+for (const failure of ["eof", "oversized", "timeout", "abort"] as const) test(`proxy ${failure} failure settles and closes the owned connection`, async () => {
+ let closed!: () => void, accepted!: () => void;
+ const closedEvent = new Promise(resolve => { closed = resolve; }), acceptedEvent = new Promise(resolve => { accepted = resolve; });
+ const server = createTcp(socket => {
+ socket.once("close", closed); socket.once("data", () => {
+ accepted();
+ if (failure === "eof") socket.end();
+ if (failure === "oversized") socket.write(Buffer.alloc(65537, 65));
+ });
+ });
+ const proxy = await listenFixture(server), abort = new AbortController();
+ try {
+ const dial = dialDesktopUpstream({ proxy: `http://127.0.0.1:${proxy.port}`, timeoutMs: failure === "abort" ? 2000 : 500, signal: abort.signal });
+ const settled = dial.then(() => null, error => error as Error);
+ await acceptedEvent; if (failure === "abort") abort.abort();
+ const at = Date.now(), result = await settled;
+ expect(result?.message).toBe("desktop_upstream_connection_failed");
+ if (failure === "abort") expect(Date.now() - at).toBeLessThan(500);
+ await closedEvent;
+ } finally { await proxy.close(); }
+}, 3000);
diff --git a/tests/server/management-desktop-compatibility-routes.test.ts b/tests/server/management-desktop-compatibility-routes.test.ts
new file mode 100644
index 00000000000..287aa474447
--- /dev/null
+++ b/tests/server/management-desktop-compatibility-routes.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from "bun:test";
+import { handleDesktopCompatibilityRoutes } from "../../src/server/management/desktop-compatibility-routes";
+import type { ManagementContext } from "../../src/server/management/context";
+import type { DesktopCertificateService } from "../../src/codex/desktop-compatibility/certificate-service";
+import { handleManagementAPI } from "../../src/server/management-api";
+import type { OcxConfig } from "../../src/types";
+
+const url = new URL("http://127.0.0.1:10100/api/codex/desktop-compatibility/certificate");
+function fixture(method: string, body?: object, principal: ManagementContext["principal"] = "gui-session", loopback = true) {
+ const calls: string[] = [];
+ const status = { supported: true, state: "prepared" as const, busy: null };
+ const service: DesktopCertificateService = { status: async () => { calls.push("status"); return status; },
+ prepare: async () => { calls.push("prepare"); return status; }, trust: async fp => { calls.push("trust:" + fp); return status; },
+ removeTrust: async fp => { calls.push("remove:" + fp); return status; }, renew: async fp => { calls.push("renew:" + fp); return status; } };
+ const ctx = { req: new Request(url, { method, headers: { host: url.host, "content-type": "application/json" }, ...(body ? { body: JSON.stringify(body) } : {}) }),
+ url, principal, trustedLoopbackIngress: loopback, deps: { desktopCertificateService: service } } as ManagementContext;
+ return { calls, ctx };
+}
+test("status reads do not invoke setup or OS trust", async () => {
+ const io = fixture("GET", undefined, "admin-token"); expect((await handleDesktopCompatibilityRoutes(io.ctx))?.status).toBe(200);
+ expect(io.calls).toEqual(["status"]);
+});
+test("only authenticated local dashboard mutations reach the service", async () => {
+ for (const [principal, loopback] of [["admin-token", true], ["gui-session", false], [undefined, true]] as const) {
+ const io = fixture("POST", { action: "prepare", confirmed: true }, principal, loopback);
+ if (principal === undefined) io.ctx.principal = undefined;
+ expect((await handleDesktopCompatibilityRoutes(io.ctx))?.status).toBe(403); expect(io.calls).toEqual([]);
+ }
+});
+test("explicit confirmation and exact fingerprint are validated before any side effect", async () => {
+ for (const body of [{ action: "prepare" }, { action: "trust", confirmed: true }, { action: "trust", confirmed: true, fingerprint: "wrong" },
+ { action: "prepare", confirmed: true, injected: true }, { action: ["prepare"], confirmed: true }]) {
+ const io = fixture("POST", body); expect((await handleDesktopCompatibilityRoutes(io.ctx))?.status).toBe(400); expect(io.calls).toEqual([]);
+ }
+ const valid = fixture("POST", { action: "trust", confirmed: true, fingerprint: "A".repeat(64) });
+ expect((await handleDesktopCompatibilityRoutes(valid.ctx))?.status).toBe(200); expect(valid.calls).toEqual(["trust:" + "A".repeat(64)]);
+});
+test("unexpected errors never expose process output or credential strings", async () => {
+ const io = fixture("POST", { action: "prepare", confirmed: true });
+ io.ctx.deps.desktopCertificateService!.prepare = async () => { throw new Error("synthetic-private-key-output"); };
+ const response = await handleDesktopCompatibilityRoutes(io.ctx); expect(response?.status).toBe(409);
+ expect(await response!.json()).toEqual({ ok: false, error: "operation_failed" });
+});
+
+test("renewal requires fresh fingerprint and explicit local dashboard confirmation", async () => {
+ const invalid = fixture("POST", { action: "renew", confirmed: true });
+ expect((await handleDesktopCompatibilityRoutes(invalid.ctx))?.status).toBe(400); expect(invalid.calls).toEqual([]);
+ const valid = fixture("POST", { action: "renew", confirmed: true, fingerprint: "A".repeat(64) });
+ expect((await handleDesktopCompatibilityRoutes(valid.ctx))?.status).toBe(200); expect(valid.calls).toEqual(["renew:" + "A".repeat(64)]);
+});
+test("the real management dispatcher reaches setup with principal and ingress intact", async () => {
+ const io = fixture("POST", { action: "prepare", confirmed: true });
+ const response = await handleManagementAPI(io.ctx.req, url, { providers: {} } as OcxConfig,
+ io.ctx.deps, "gui-session", undefined, { trustedLoopback: true });
+ expect(response?.status).toBe(200); expect(io.calls).toEqual(["prepare"]);
+});
diff --git a/tests/server/management-desktop-compatibility-runtime-routes.test.ts b/tests/server/management-desktop-compatibility-runtime-routes.test.ts
new file mode 100644
index 00000000000..2610f63c8b3
--- /dev/null
+++ b/tests/server/management-desktop-compatibility-runtime-routes.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from "bun:test";
+import { handleDesktopCompatibilityRuntimeRoutes } from "../../src/server/management/desktop-compatibility-runtime-routes";
+import { createDesktopCompatibilityRuntime } from "../../src/codex/desktop-compatibility/runtime";
+import type { ManagementContext } from "../../src/server/management/context";
+import { handleManagementAPI } from "../../src/server/management-api";
+import type { OcxConfig } from "../../src/types";
+
+const url = new URL("http://127.0.0.1:10100/api/codex/desktop-compatibility/runtime");
+function fixture(method: string, body?: unknown) {
+ const calls: string[] = [];
+ const runtime = createDesktopCompatibilityRuntime({ platform: "linux" });
+ runtime.start = async () => { calls.push("start"); return runtime.status(); };
+ runtime.stop = async () => { calls.push("stop"); return runtime.status(); };
+ const ctx = { req: new Request(url, { method, headers: { host: url.host, "content-type": "application/json" },
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }) }), url,
+ deps: { desktopCompatibilityRuntime: runtime }, principal: "gui-session", trustedLoopbackIngress: true } as ManagementContext;
+ return { ctx, calls, runtime };
+}
+test("runtime status is inert and the management dispatcher preserves local consent", async () => {
+ const io = fixture("GET");
+ expect((await handleDesktopCompatibilityRuntimeRoutes(io.ctx))?.status).toBe(200); expect(io.calls).toEqual([]);
+ const confirmed = fixture("POST", { action: "start", confirmed: true });
+ const response = await handleManagementAPI(confirmed.ctx.req, url, { providers: {} } as OcxConfig, confirmed.ctx.deps, "gui-session", undefined, { trustedLoopback: true });
+ expect(response?.status).toBe(200); expect(confirmed.calls).toEqual(["start"]);
+});
+test("remote/admin-token callers and ambiguous scope cannot start or alter runtime", async () => {
+ for (const change of [{ principal: "admin-token" }, { trustedLoopbackIngress: false }]) {
+ const io = fixture("POST", { action: "start", confirmed: true }); Object.assign(io.ctx, change);
+ expect((await handleDesktopCompatibilityRuntimeRoutes(io.ctx))?.status).toBe(403); expect(io.calls).toEqual([]);
+ }
+ for (const body of [{ action: "start" }, { action: ["start"], confirmed: true }, { action: "apply", confirmed: true },
+ { action: "start", confirmed: true, accountWideConsent: true }, { action: "start", confirmed: true, model: "fixture" }]) {
+ const io = fixture("POST", body); expect((await handleDesktopCompatibilityRuntimeRoutes(io.ctx))?.status).toBe(400); expect(io.calls).toEqual([]);
+ }
+});
+test("runtime failures report safe codes and never leak subprocess or credential data", async () => {
+ const io = fixture("POST", { action: "start", confirmed: true });
+ io.runtime.start = async () => { throw new Error("synthetic-private-data"); };
+ const response = await handleDesktopCompatibilityRuntimeRoutes(io.ctx);
+ expect(response?.status).toBe(409); expect(await response!.text()).not.toContain("synthetic-private-data");
+});
diff --git a/tests/server/management-desktop-compatibility-settings.test.ts b/tests/server/management-desktop-compatibility-settings.test.ts
new file mode 100644
index 00000000000..e0ad711ebe9
--- /dev/null
+++ b/tests/server/management-desktop-compatibility-settings.test.ts
@@ -0,0 +1,80 @@
+import { afterEach, beforeEach, expect, test } from "bun:test";
+import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createDesktopStartupSettings } from "../../src/codex/desktop-compatibility/startup-settings";
+import { handleDesktopCompatibilitySettingsRoutes } from "../../src/server/management/desktop-compatibility-settings-routes";
+import { handleManagementAPI } from "../../src/server/management-api";
+import { armClaudeCodeBaseline, loadConfig, saveConfigPreservingClaudeCode } from "../../src/config";
+import { mutatePersistedConfig, setPersistedConfigMutationBeforeCommitForTests } from "../../src/config/persisted-mutation";
+import { setIcaclsRunnerForTests, setAsyncIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl";
+import { flushConfigDirHardeningAndReaps } from "../../src/config/paths";
+import { removeTreeWithRetry } from "../helpers/remove-tree";
+import type { ManagementContext } from "../../src/server/management/context";
+import type { OcxConfig } from "../../src/types";
+let root = "", previous: string | undefined;
+const initial = { port: 10100, defaultProvider: "fixture", providers: { fixture: { adapter: "openai-chat", baseUrl: "https://example.test/v1" } } };
+const success = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" });
+const read = () => JSON.parse(readFileSync(join(root, "config.json"), "utf8"));
+const write = (value: unknown) => writeFileSync(join(root, "config.json"), JSON.stringify(value));
+beforeEach(() => {
+ previous = process.env.OPENCODEX_HOME; root = mkdtempSync(join(tmpdir(), "ocx-desktop-setting-")); process.env.OPENCODEX_HOME = root;
+ setIcaclsRunnerForTests(success); setAsyncIcaclsRunnerForTests(async () => success()); write(initial);
+});
+afterEach(async () => {
+ setPersistedConfigMutationBeforeCommitForTests(null); await flushConfigDirHardeningAndReaps(root);
+ if (previous === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previous;
+ setIcaclsRunnerForTests(null); setAsyncIcaclsRunnerForTests(null); removeTreeWithRetry(root);
+});
+test("field-scoped write preserves unrelated concurrent settings and rejects a stale preference", () => {
+ const service = createDesktopStartupSettings(), first = service.status();
+ expect(first.startOnProxyStart).toBe(false);
+ setPersistedConfigMutationBeforeCommitForTests(() => write({ ...read(), logLevel: "debug" }));
+ const enabled = service.set(true, first.revision);
+ expect(enabled.startOnProxyStart).toBe(true); expect(read().logLevel).toBe("debug"); expect(read().providers).toEqual(initial.providers);
+ expect(() => service.set(false, first.revision)).toThrow("settings_changed"); expect(read().desktopCompatibility.startOnProxyStart).toBe(true);
+ expect(service.set(true, enabled.revision)).toEqual(enabled);
+});
+test("an unrelated later live save does not revert the persisted next-start preference", () => {
+ const live = loadConfig(); armClaudeCodeBaseline(live);
+ const service = createDesktopStartupSettings({ liveConfig: live }); service.set(true, service.status().revision);
+ live.logLevel = "debug"; saveConfigPreservingClaudeCode(live);
+ expect(read().desktopCompatibility).toEqual({ startOnProxyStart: true }); expect(read().logLevel).toBe("debug");
+ write({ ...read(), desktopCompatibility: { startOnProxyStart: false } });
+ live.logLevel = "info"; saveConfigPreservingClaudeCode(live);
+ expect(read().desktopCompatibility).toEqual({ startOnProxyStart: false });
+});
+test("a publication-side error stays an error but cannot let a later save undo committed intent", () => {
+ const live = loadConfig(); armClaudeCodeBaseline(live);
+ const service = createDesktopStartupSettings({ liveConfig: live, mutate: callback => {
+ mutatePersistedConfig(callback); throw new Error("fixture post-publication failure");
+ } });
+ expect(() => service.set(true, service.status().revision)).toThrow("fixture post-publication failure");
+ expect(service.status().startOnProxyStart).toBe(true);
+ live.logLevel = "debug"; saveConfigPreservingClaudeCode(live); expect(read().desktopCompatibility.startOnProxyStart).toBe(true);
+});
+test("invalid settings are preserved and a competing preference blocks replay", () => {
+ const service = createDesktopStartupSettings(), first = service.status();
+ setPersistedConfigMutationBeforeCommitForTests(() => write({ ...read(), desktopCompatibility: { startOnProxyStart: true } }));
+ expect(() => service.set(true, first.revision)).toThrow("settings_changed");
+ const malformed = { ...initial, desktopCompatibility: { startOnProxyStart: true, apply: true } }; write(malformed);
+ expect(() => service.status()).toThrow("settings_invalid"); expect(read()).toEqual(malformed);
+});
+function request(method: string, body?: unknown): ManagementContext {
+ const url = new URL("http://127.0.0.1:10100/api/codex/desktop-compatibility/settings");
+ return { url, req: new Request(url, { method, headers: { host: url.host, "content-type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }),
+ config: initial, principal: "gui-session", trustedLoopbackIngress: true, deps: { desktopStartupSettings: createDesktopStartupSettings() } } as unknown as ManagementContext;
+}
+test("management dispatcher enforces local confirmation and reads back the saved preference", async () => {
+ const service = createDesktopStartupSettings(), before = service.status();
+ for (const change of [{ principal: "admin-token" }, { trustedLoopbackIngress: false }]) {
+ const ctx = request("POST", { confirmed: true, startOnProxyStart: true, revision: before.revision }); Object.assign(ctx, change);
+ expect((await handleDesktopCompatibilitySettingsRoutes(ctx))?.status).toBe(403);
+ }
+ const invalid = request("POST", { confirmed: true, startOnProxyStart: "true", revision: before.revision });
+ expect((await handleDesktopCompatibilitySettingsRoutes(invalid))?.status).toBe(400);
+ expect(service.status()).toEqual(before);
+ const ctx = request("POST", { confirmed: true, startOnProxyStart: true, revision: before.revision });
+ const response = await handleManagementAPI(ctx.req, ctx.url, ctx.config as OcxConfig, ctx.deps, "gui-session", undefined, { trustedLoopback: true });
+ expect(response?.status).toBe(200); expect((await response!.json()).settings).toEqual(service.status()); expect(service.status().startOnProxyStart).toBe(true);
+});
diff --git a/tests/server/server-desktop-compatibility-startup.test.ts b/tests/server/server-desktop-compatibility-startup.test.ts
new file mode 100644
index 00000000000..d479d9dbb6c
--- /dev/null
+++ b/tests/server/server-desktop-compatibility-startup.test.ts
@@ -0,0 +1,111 @@
+import { expect, test } from "bun:test";
+import { scheduleDesktopCompatibilityStartup } from "../../src/server/index/desktop-compatibility-startup";
+import { createDesktopCompatibilityService } from "../../src/codex/desktop-compatibility/service";
+import { createDesktopCompatibilityRuntime } from "../../src/codex/desktop-compatibility/runtime";
+import { getDefaultConfig, validateConfigCandidate } from "../../src/config";
+import { configSchema } from "../../src/config/schema/config-schema";
+import { nativeCompatibilityOwner } from "../../src/codex/desktop-compatibility/routing-binding";
+import { createReadinessGate } from "../../src/server/readiness";
+
+test("startup captures the actual bound hostname and clears that owner on shutdown", async () => {
+ const config = { ...getDefaultConfig(), hostname: "192.0.2.10" };
+ const handle = scheduleDesktopCompatibilityStartup(config, { boundPort: 12001, boundHostname: "127.0.0.1", loopbackPort: 12002, testGuard: true });
+ try { expect(nativeCompatibilityOwner()).toMatchObject({ hostname: "127.0.0.1", port: 12001, loopbackPort: 12002 }); }
+ finally { await handle.shutdown(); }
+ expect(nativeCompatibilityOwner()).toBeNull();
+});
+
+test("startup preference is absent by default, strict for writes and safely disabled on malformed disk input", () => {
+ expect(getDefaultConfig().desktopCompatibility).toBeUndefined();
+ const valid = { ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } };
+ expect(validateConfigCandidate(valid).ok).toBe(true);
+ for (const setting of [{ startOnProxyStart: "true" }, { startOnProxyStart: true, apply: true }, { enabled: true }]) {
+ const value = { ...getDefaultConfig(), desktopCompatibility: setting };
+ expect(validateConfigCandidate(value).ok).toBe(false); expect(configSchema.parse(value).desktopCompatibility).toBeUndefined();
+ }
+});
+test("off, unsupported, guarded, sibling and managed-client starts never load the optional runtime", async () => {
+ let loads = 0;
+ for (const patch of [{ setting: false }, { platform: "linux" }, { testGuard: true }, { sibling: true }, { client: true }]) {
+ const config = { ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: patch.setting ?? true }, ...(patch.client ? { runtimeRole: "client" as const } : {}) };
+ const handle = scheduleDesktopCompatibilityStartup(config, { platform: patch.platform ?? "win32", testGuard: patch.testGuard ?? false, sibling: patch.sibling ?? false,
+ load: async () => { loads++; throw new Error("must not load"); } });
+ await Promise.resolve(); await Promise.resolve();
+ await handle.shutdown();
+ }
+ expect(loads).toBe(0);
+});
+test("failed optional startup reports a generic diagnostic without failing proxy lifecycle", async () => {
+ const warnings: string[] = [];
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, load: async () => { throw new Error("fixture-sensitive-error"); }, warn: text => warnings.push(text) });
+ await Promise.resolve(); await Promise.resolve(); await handle.shutdown();
+ expect(warnings).toHaveLength(1); expect(warnings[0]).toContain("did not start"); expect(warnings[0]).not.toContain("fixture-sensitive-error");
+});
+test("automatic startup and management share one runtime and teardown waits for pending start", async () => {
+ let started = 0, stopped = 0, created = 0, ready!: () => void;
+ const waiting = new Promise(resolve => { ready = resolve; });
+ const service = createDesktopCompatibilityService(() => {
+ created++; const runtime = createDesktopCompatibilityRuntime({ platform: "linux" });
+ runtime.start = async () => { started++; await waiting; return runtime.status(); };
+ runtime.stop = async () => { stopped++; return runtime.status(); };
+ return runtime;
+ });
+ const module = { getDesktopCompatibilityRuntime: () => service.get(), shutdownDesktopCompatibility: () => service.shutdown() };
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, load: async () => module });
+ await Promise.resolve(); await Promise.resolve();
+ expect(started).toBe(1); expect(created).toBe(1);
+ await expect(service.get().start()).rejects.toThrow("busy");
+ let finished = false; const stopping = handle.shutdown().then(() => { finished = true; });
+ await Promise.resolve(); expect(finished).toBe(false); expect(stopped).toBe(0);
+ ready(); await stopping; expect(stopped).toBe(1); expect(service.get()).toBe(service.get());
+ await expect(service.get().start()).rejects.toThrow("stopping");
+});
+test("shutdown before module load finishes prevents late service activation", async () => {
+ let release!: () => void, starts = 0, shutdowns = 0;
+ const waiting = new Promise(resolve => { release = resolve; });
+ const runtime = createDesktopCompatibilityRuntime({ platform: "linux" });
+ runtime.start = async () => { starts++; return runtime.status(); };
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, load: async () => {
+ await waiting; return { getDesktopCompatibilityRuntime: () => runtime, shutdownDesktopCompatibility: async () => { shutdowns++; } };
+ } });
+ await Promise.resolve(); const closing = handle.shutdown(); release(); await closing;
+ expect(starts).toBe(0); expect(shutdowns).toBe(1);
+});
+
+test("automatic observation waits for native configuration readiness", async () => {
+ const readiness = createReadinessGate(); let starts = 0, loads = 0;
+ const runtime = createDesktopCompatibilityRuntime({ platform: "linux" });
+ runtime.start = async () => { starts++; return runtime.status(); };
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, readiness,
+ load: async () => { loads++; return { getDesktopCompatibilityRuntime: () => runtime, shutdownDesktopCompatibility: async () => {} }; } });
+ try {
+ await Bun.sleep(20); expect(loads).toBe(0); expect(starts).toBe(0);
+ readiness.markReady(); await Bun.sleep(150);
+ expect(loads).toBe(1); expect(starts).toBe(1);
+ } finally { await handle.shutdown(); }
+});
+
+test("shutdown cancels pending readiness and never starts a late observation", async () => {
+ const readiness = createReadinessGate(); let loads = 0;
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, readiness,
+ load: async () => { loads++; throw new Error("must not load"); }, warn: () => {} });
+ await Promise.resolve(); await handle.shutdown(); readiness.markReady(); await Bun.sleep(150);
+ expect(loads).toBe(0);
+});
+
+test("failed or timed-out readiness leaves optional observation off", async () => {
+ for (const failed of [true, false]) {
+ const readiness = createReadinessGate(); if (failed) readiness.markFailed();
+ let loads = 0; const warnings: string[] = [];
+ const handle = scheduleDesktopCompatibilityStartup({ ...getDefaultConfig(), desktopCompatibility: { startOnProxyStart: true } },
+ { platform: "win32", testGuard: false, sibling: false, readiness, readinessTimeoutMs: 1,
+ load: async () => { loads++; throw new Error("must not load"); }, warn: value => warnings.push(value) });
+ await Bun.sleep(150); await handle.shutdown();
+ expect(loads).toBe(0); expect(warnings).toHaveLength(1);
+ }
+});
diff --git a/tests/test-layout-tooling.test.ts b/tests/test-layout-tooling.test.ts
index c675c9ce639..e063c0fe364 100644
--- a/tests/test-layout-tooling.test.ts
+++ b/tests/test-layout-tooling.test.ts
@@ -20,10 +20,21 @@ import {
type Layout,
} from "../scripts/test-layout/schema";
-// Independent oracle: the basename -> directory table from devlog 001 §2.D, committed as a
-// fixture. The layout guard shares the resolver with the mover, so a resolver defect could move
-// a file to the wrong place and bless it; this fixture is the second opinion that catches it.
-const EXPECTED = JSON.parse(readFileSync(repoPath("tests", "fixtures", "test-layout-expected.json"), "utf8")) as Record;
+// Independent oracle: the basename -> directory table from devlog 001 §2.D, committed as fixture
+// shards. The layout guard shares the resolver with the mover, so a resolver defect could move a
+// file to the wrong place and bless it; these fixtures are the second opinion that catches it.
+const EXPECTED_FIXTURE_PARTS = [
+ "test-layout-expected.json",
+ "test-layout-expected-additional.json",
+] as const;
+const EXPECTED: Record = {};
+for (const part of EXPECTED_FIXTURE_PARTS) {
+ const entries = JSON.parse(readFileSync(repoPath("tests", "fixtures", part), "utf8")) as Record;
+ for (const [file, domain] of Object.entries(entries)) {
+ if (Object.hasOwn(EXPECTED, file)) throw new Error(`duplicate expected test-layout entry: ${file}`);
+ EXPECTED[file] = domain;
+ }
+}
describe("rewriteSpecifier", () => {
const forms = [