/* Codex web host shim — browser-only replacement for the Electron preload. * * Loaded as a classic script before the module entry. It provides the two * pieces of the Electron shell the renderer cannot live without: * * window.electronBridge — the preload API surface. Only the members the * renderer actually probes are meaningful; the rest * resolve to inert promises so .then/.catch chains * stay valid. * the app-server bridge — request envelopes sent through * sendMessageFromView are forwarded to the local * codex app server and answered on the window * message bus, which is where the renderer listens. */ (() => { const noop = () => {}; globalThis.__codexOut = []; globalThis.__codexIn = []; let rpcCount = 0; // ------------------------------------------------------------------ inbound // The renderer validates inbound messages with `event.source === null` (it // skips the origin check in that case), which is exactly what a synthesized // MessageEvent gives us. const dispatchInbound = (payload) => { globalThis.__codexIn.push(payload); window.dispatchEvent(new MessageEvent("message", { data: payload })); }; const forwardRequest = (msg) => { const { id, method, params } = msg.request; rpcCount += 1; if (rpcCount <= 500) console.info("[codex-web] rpc ->", method); fetch("/__codex_rpc", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method: method, params: params ?? {} }), }) .then((res) => res.json()) .then((body) => { // The renderer's app-server request client reads the reply from // `message: { id, result | error }`. `response` is kept alongside it // because the app-host side of the same bus uses that spelling. const message = body && body.error ? { id: id, error: { code: body.error.code ?? -32000, message: body.error.message ?? "rpc failed" } } : { id: id, result: body ? body.result : null }; dispatchInbound({ type: "mcp-response", hostId: msg.hostId, message: message, response: message, requestMethod: method, receivedAtMs: Date.now(), }); }) .catch((err) => { const message = { id: id, error: { code: -32000, message: String((err && err.message) || err) } }; dispatchInbound({ type: "mcp-response", hostId: msg.hostId, message: message, response: message, requestMethod: method, receivedAtMs: Date.now(), }); }); return Promise.resolve(); }; // ---------------------------------------------------------------- events // Everything the app server says on its own initiative — streaming deltas, // turn progress, server-initiated approval prompts — arrives on this // long-poll. The renderer subscribes to it as `mcp-notification`. globalThis.__codexEvents = []; let polling = false; const pumpEvents = async () => { if (polling) return; polling = true; while (polling) { try { const res = await fetch("/__codex_events", { cache: "no-store" }); const body = await res.json(); for (const event of body.events || []) { globalThis.__codexEvents.push(event); if (globalThis.__codexEvents.length > 400) globalThis.__codexEvents.shift(); if (event.id != null) { // Server-initiated request: the renderer owns the answer path, which // this shim does not model yet. Keep it visible instead of dropping. globalThis.__codexServerRequests = globalThis.__codexServerRequests || []; globalThis.__codexServerRequests.push(event); continue; } dispatchInbound({ type: "mcp-notification", hostId: "local", method: event.method, params: event.params ?? {}, }); } } catch { await new Promise((r) => setTimeout(r, 1000)); } } }; pumpEvents(); // ------------------------------------------------------------- electron API // Synchronous getters first: callers treat these as plain values. const SYNC = { getInitialSidebarBootstrap: () => null, getConfig: () => null, getPlatform: () => "darwin", getLocale: () => "zh-CN", getVersion: () => "26.1021.31345", isPackaged: () => true, getWindowType: () => "electron", getZoomLevel: () => 0, isDeviceCheckSupported: () => true, isIntelMacBuild: () => false, getSystemThemeVariant: () => "light", subscribeToSystemThemeVariant: () => noop, getBuildFlavor: () => "codex", getAppSessionId: () => "codex-web-local", getDesktopUserAgent: () => navigator.userAgent, getSentryInitOptions: () => ({ appVersion: "26.1021.31345", buildFlavor: "codex" }), getSharedObjectSnapshotValue: () => undefined, }; const makeBridge = () => new Proxy(SYNC, { get(target, prop) { if (prop in target) return target[prop]; if (prop === "then") return undefined; // never a thenable if (typeof prop === "symbol") return undefined; if (prop === "sendMessageFromView") { return (msg) => { globalThis.__codexOut.push(msg); if (msg && msg.type === "mcp-request" && msg.request && typeof msg.request.method === "string") { return forwardRequest(msg); } return Promise.resolve(); }; } // Unknown member: an inert async function, so feature probes see // "present" and promise chains stay valid. return () => Promise.resolve(); }, has: () => true, }); if (typeof window.electronBridge === "undefined") window.electronBridge = makeBridge(); if (typeof window.codexWindowType === "undefined") window.codexWindowType = "electron"; if (typeof window.__SENTRY_IPC__ === "undefined") window.__SENTRY_IPC__ = { sendMessage: noop }; window.__codexDesktop = window.__codexDesktop || {}; // ------------------------------------------------------------- offline net // Nothing leaves the machine. Absolute URLs get a well-formed empty JSON body // so callers fail softly instead of hitting the CSP. The remaining browser // transports — XHR, sendBeacon, WebSocket, EventSource — are stubbed the same // way: local requests pass through, everything else succeeds locally with // empty data. const realFetch = window.fetch.bind(window); const isLocal = (url) => !url || url.startsWith("/") || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith(location.origin); window.fetch = (input, init) => { const url = typeof input === "string" ? input : (input && input.url) || ""; if (isLocal(url)) return realFetch(input, init); console.info("[codex-web] outbound:", (init?.method || "GET"), url.slice(0, 140)); const json = (obj, status = 200) => Promise.resolve(new Response(typeof obj === "string" ? obj : JSON.stringify(obj), { status, headers: { "content-type": "application/json" }, })); // Statsig telemetry hosts. The SDK requires its initialize payload to // carry `has_updates` (see _typedJsonParse), so hand back a valid empty // evaluation set — every gate then resolves to "off" locally and the SDK // completes initialization instead of retrying forever. if (/ab\.chatgpt\.com|api\.oaistatsig\.com|statsigcdn\.openai\.com/i.test(url)) { if (/\/initialize/i.test(url)) { return json({ has_updates: true, hash_used: "djb2", time: Date.now(), feature_gates: {}, dynamic_configs: {}, layer_configs: {}, param_stores: {}, }); } if (/sdk_exception/i.test(url)) { return Promise.resolve(new Response(null, { status: 204 })); } // rgstr / download_config_specs: accepted-and-empty. return Promise.resolve(new Response(null, { status: 202 })); } // Everything else: generic empty JSON success. return json({}); }; // XMLHttpRequest: same policy. A cross-origin send completes immediately // with `{}` and status 200, on the microtask queue so ordering stays sane. const XHR = window.XMLHttpRequest; if (XHR && XHR.prototype) { const openOrig = XHR.prototype.open; const sendOrig = XHR.prototype.send; XHR.prototype.open = function (method, url, ...rest) { this.__codexUrl = String(url ?? ""); return openOrig.call(this, method, url, ...rest); }; XHR.prototype.send = function (body) { if (isLocal(this.__codexUrl)) return sendOrig.call(this, body); const payload = this.responseType === "json" ? {} : "{}"; queueMicrotask(() => { try { Object.defineProperty(this, "readyState", { value: 4, configurable: true }); Object.defineProperty(this, "status", { value: 200, configurable: true }); Object.defineProperty(this, "statusText", { value: "OK", configurable: true }); Object.defineProperty(this, "responseText", { value: payload, configurable: true }); Object.defineProperty(this, "response", { value: payload, configurable: true }); Object.defineProperty(this, "responseURL", { value: this.__codexUrl, configurable: true }); this.dispatchEvent(new Event("readystatechange")); this.dispatchEvent(new ProgressEvent("load")); this.dispatchEvent(new ProgressEvent("loadend")); } catch { /* the request object went away; nothing to satisfy */ } }); }; } // sendBeacon is fire-and-forget by contract; "delivered" locally means dropped. try { if (navigator.sendBeacon) navigator.sendBeacon = () => true; } catch { /* some engines make it a getter; the CSP covers those */ } // WebSocket: hand back a socket that is already closed. No network dial, no // browser console noise, and well-behaved clients see a terminal state. const RealWS = window.WebSocket; if (RealWS) { class ClosedSocket extends EventTarget { constructor(url) { super(); this.url = String(url ?? ""); this.readyState = 3; // CLOSED this.bufferedAmount = 0; this.extensions = ""; this.protocol = ""; this.binaryType = "blob"; queueMicrotask(() => { this.dispatchEvent(new Event("error")); this.dispatchEvent(new CloseEvent("close", { wasClean: true, code: 1000 })); }); } send() { throw new DOMException("The socket is closed.", "InvalidStateError"); } close() {} } for (const k of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]) ClosedSocket[k] = RealWS[k]; window.WebSocket = ClosedSocket; } // EventSource: a stream that closes immediately, for the same reason. if (window.EventSource) { class ClosedStream extends EventTarget { constructor(url) { super(); this.url = String(url ?? ""); this.readyState = 2; // CLOSED this.withCredentials = false; queueMicrotask(() => this.dispatchEvent(new Event("error"))); } close() {} } window.EventSource = ClosedStream; } console.info("[codex-web] offline host shim installed"); })(); // ------------------------------------------------------------- config preseed // The scope-query bridge only fetches from mounted React readers, and the // config/effective + permission-profiles queries have no mounted reader in a // browser fallback (no project tree, no workspace picker). Prime both from the // local app server up front so the composer's agent-mode resolution sees real // data on first render instead of a query that would stay pending forever. (() => { const rpc = (method, params) => fetch("/__codex_rpc", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method, params: params ?? null }), }) .then((r) => r.json()) .then((b) => (b && b.result ? b.result : null)) .catch(() => null); globalThis.__codexCfg = null; globalThis.__codexPerms = null; rpc("config/read", { includeLayers: true, cwd: null }).then((r) => { if (r && r.config) { globalThis.__codexCfg = r; console.info("[codex-web] config preseeded"); } }); rpc("permissionProfile/list", { cursor: null, limit: 50, cwd: null }).then((r) => { if (r && Array.isArray(r.data)) { globalThis.__codexPerms = r.data; console.info("[codex-web] permission profiles preseeded"); } }); })();