From aae64801e9a9592ba3bce6a69d8fe42735dc5dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=83=81=ED=9B=88?= Date: Sun, 2 Aug 2026 21:49:24 +0900 Subject: [PATCH] Preserve OCX route for full MCP setup --- launcher/electron/main.cjs | 11 +++++++---- launcher/electron/runtime.cjs | 7 ++++++- launcher/package.json | 2 +- launcher/tests/runtime-host.test.cjs | 9 ++++++++- package.json | 2 +- scripts/install.sh | 2 +- src/cli.ts | 8 +++++++- src/setup.ts | 29 ++++++++++++++++++++-------- src/version.ts | 2 +- tests/setup-lifecycle.test.ts | 11 ++++++++++- 10 files changed, 63 insertions(+), 20 deletions(-) diff --git a/launcher/electron/main.cjs b/launcher/electron/main.cjs index 3120ff0e1..a0f30ba1d 100644 --- a/launcher/electron/main.cjs +++ b/launcher/electron/main.cjs @@ -24,7 +24,7 @@ const { installProcessDiagnosticGuards, registerLoggedIpc, } = require("./logging.cjs"); -const { RuntimeHost } = require("./runtime.cjs"); +const { RuntimeHost, launcherRuntimeAllowedForRoute } = require("./runtime.cjs"); const { ensurePackagedRuntime } = require("./runtime-install.cjs"); const { RuntimeSupervisor } = require("./runtime-supervisor.cjs"); const { createStateStore, validateSidebarState } = require("./state.cjs"); @@ -511,7 +511,7 @@ function registerIpc({ logger, stateStore }) { runtimeKey: typeof input?.runtimeKey === "string" ? input.runtimeKey : "", replace: input?.replace === true, }); - stateStore.update({ mcpRuntimeInstalled: true, mcpGuideStep: 2, codexRestartRequired: true }); + stateStore.update({ mcpRuntimeInstalled: true, mcpGuideStep: 2, codexRestartRequired: false }); return { ok: true, stdout: result.stdout }; }); handle("launcher:set-mcp-step", (_event, step) => { @@ -725,7 +725,9 @@ async function start() { const state = stateStore.update({ bridgeEnabled: route.active }); send("launcher:state-changed", state); } - if (!route.active) return { status: "bridge-disabled" }; + let config = null; + try { config = runtimeSupervisor.readConfig(); } catch {} + if (!launcherRuntimeAllowedForRoute(route, config)) return { status: "bridge-disabled" }; } } catch (error) { logger.warn("bridge.route_status_failed", { @@ -752,7 +754,8 @@ async function start() { const state = stateStore.update(patch); send("launcher:state-changed", state); } - startCatalogVerificationMonitor({ logger, stateStore }); + if (current.bridgeEnabled) startCatalogVerificationMonitor({ logger, stateStore }); + else stopCatalogVerificationMonitor(); return; } if (runtime.status === "not-configured") { diff --git a/launcher/electron/runtime.cjs b/launcher/electron/runtime.cjs index f6493f629..78f8b6117 100644 --- a/launcher/electron/runtime.cjs +++ b/launcher/electron/runtime.cjs @@ -12,6 +12,10 @@ const MAX_CAPTURE_BYTES = 8 * 1024 * 1024; const MAX_RUNTIME_LOG_LINE_CHARS = 64 * 1024; const CORE_SETUP_TIMEOUT_MS = 5 * 60_000; const MCP_SETUP_TIMEOUT_MS = 10 * 60_000; + +function launcherRuntimeAllowedForRoute(route, config) { + return route?.active === true || config?.mode === "full"; +} const UNINSTALL_TIMEOUT_MS = 2 * 60_000; const MAX_CHECKPOINT_FILE_BYTES = 16 * 1024 * 1024; @@ -677,6 +681,7 @@ class RuntimeHost { "--full", "--browser-host-descriptor", this.browserDescriptorPath, + "--preserve-codex-route", ]; if (reuseSavedCredentials) { args.push("--acknowledge-unofficial", "--restart-service"); @@ -777,4 +782,4 @@ class RuntimeHost { } } -module.exports = { RuntimeHost }; +module.exports = { RuntimeHost, launcherRuntimeAllowedForRoute }; diff --git a/launcher/package.json b/launcher/package.json index 41be54b9e..a938cc97a 100644 --- a/launcher/package.json +++ b/launcher/package.json @@ -1,6 +1,6 @@ { "name": "codex-web-gpt-launcher", - "version": "1.0.1-ko.3", + "version": "1.0.1-ko.4", "private": true, "description": "Desktop control center for Codex ChatGPT Web", "author": "miuuyy; Korean localization by AgenticLab-SH", diff --git a/launcher/tests/runtime-host.test.cjs b/launcher/tests/runtime-host.test.cjs index 8130969d9..c177e8aab 100644 --- a/launcher/tests/runtime-host.test.cjs +++ b/launcher/tests/runtime-host.test.cjs @@ -3,7 +3,7 @@ const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); -const { RuntimeHost } = require("../electron/runtime.cjs"); +const { RuntimeHost, launcherRuntimeAllowedForRoute } = require("../electron/runtime.cjs"); function hostFor(existingConfig) { const host = new RuntimeHost({ @@ -57,6 +57,7 @@ test("MCP setup reuses valid private credentials without exposing or rewriting t "--full", "--browser-host-descriptor", "/runtime/launcher-browser.json", + "--preserve-codex-route", "--acknowledge-unofficial", "--restart-service", ]); @@ -65,6 +66,12 @@ test("MCP setup reuses valid private credentials without exposing or rewriting t } }); +test("full MCP runtime remains available while another Codex route is active", () => { + assert.equal(launcherRuntimeAllowedForRoute({ active: false }, { mode: "full" }), true); + assert.equal(launcherRuntimeAllowedForRoute({ active: false }, { mode: "browser-only" }), false); + assert.equal(launcherRuntimeAllowedForRoute({ active: true }, { mode: "browser-only" }), true); +}); + test("MCP credential replacement remains explicit and requires a complete new pair", async () => { const fixture = hostFor(null); await assert.rejects( diff --git a/package.json b/package.json index c0efd497b..c48927b95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-chatgpt-web", - "version": "1.0.1-ko.3", + "version": "1.0.1-ko.4", "private": true, "description": "A focused local Responses bridge that runs Codex tasks through a user-authenticated ChatGPT web session.", "repository": { diff --git a/scripts/install.sh b/scripts/install.sh index ea119f5c1..3f4bb5d78 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2,7 +2,7 @@ set -eu REPOSITORY="${CODEX_CHATGPT_WEB_REPOSITORY:-AgenticLab-SH/codex-chatgpt-web}" -VERSION="${CODEX_CHATGPT_WEB_VERSION:-1.0.1-ko.3}" +VERSION="${CODEX_CHATGPT_WEB_VERSION:-1.0.1-ko.4}" BIN_DIR="${CODEX_CHATGPT_WEB_BIN_DIR:-$HOME/.local/bin}" LIB_DIR="${CODEX_CHATGPT_WEB_LIB_DIR:-$HOME/.local/lib/codex-chatgpt-web}" DOC_DIR="${CODEX_CHATGPT_WEB_DOC_DIR:-$HOME/.local/share/doc/codex-chatgpt-web}" diff --git a/src/cli.ts b/src/cli.ts index 4d5cc2188..fb3478277 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -52,6 +52,7 @@ Setup options: --tunnel-id ID Existing OpenAI tunnel id (full mode) --runtime-key-file PATH File containing a Tunnels Read+Use runtime key --replace-codex-route Reversibly replace an existing openai_base_url + --preserve-codex-route Keep the current Codex route and configure the full MCP tunnel only --restart-service Explicitly restart this project's daemon after an update --login Refresh the stored ChatGPT login even if one exists --auto-approve-tool-calls Opt in to per-call browser clicks on "Allow once" prompts @@ -137,6 +138,7 @@ async function setupCommand(args: string[]): Promise { options.forceLogin = takeFlag(args, "--login"); options.autoApproveToolCalls = takeFlag(args, "--auto-approve-tool-calls"); options.replaceCodexRoute = takeFlag(args, "--replace-codex-route"); + options.preserveCodexRoute = takeFlag(args, "--preserve-codex-route"); options.restartService = takeFlag(args, "--restart-service"); assertNoArgs(args); @@ -174,7 +176,11 @@ async function setupCommand(args: string[]): Promise { stdout.write("One account-level step remains: attach the tunnel to the ChatGPT connector named in config.\n"); stdout.write("Open: https://chatgpt.com/#settings/Connectors\n"); } - stdout.write("Restart the Codex app once so its native model catalog refreshes through the installed route.\n"); + if (result.codexRestartRequired) { + stdout.write("Restart the Codex app once so its native model catalog refreshes through the installed route.\n"); + } else { + stdout.write("The existing Codex model route was preserved; no Codex restart is required for this MCP-only setup.\n"); + } } async function doctorCommand(args: string[]): Promise { diff --git a/src/setup.ts b/src/setup.ts index 15891b462..b765e832e 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -40,6 +40,7 @@ export interface SetupOptions { forceLogin?: boolean; autoApproveToolCalls?: boolean; replaceCodexRoute?: boolean; + preserveCodexRoute?: boolean; restartService?: boolean; acknowledgedUnofficial?: boolean; tunnelId?: string; @@ -53,7 +54,7 @@ export interface SetupResult { loginCreated: boolean; serviceLoaded: boolean; tunnelReady: boolean | null; - codexRestartRequired: true; + codexRestartRequired: boolean; connectorSetupRequired: boolean; } @@ -74,6 +75,13 @@ export function existingFullSetupCredentials(existing: AppConfig | undefined): E }; } +export function setupManagesCodexRoute(options: Pick): boolean { + if (options.replaceCodexRoute && options.preserveCodexRoute) { + throw new Error("Choose either --replace-codex-route or --preserve-codex-route, not both"); + } + return options.preserveCodexRoute !== true; +} + function loadExistingConfig(): AppConfig | undefined { if (!existsSync(getConfigPath())) return undefined; return loadConfigForSetup(); @@ -253,6 +261,7 @@ async function bootstrapTunnelProfile(config: AppConfig): Promise { export async function setup(options: SetupOptions): Promise { const existing = loadExistingConfig(); const config = baseConfig(existing, options); + const manageCodexRoute = setupManagesCodexRoute(options); const launcherOwned = config.browserHost === "launcher"; if (!launcherOwned && process.platform !== "darwin") { throw new Error( @@ -260,9 +269,11 @@ export async function setup(options: SetupOptions): Promise { + "Use the Codex Web GPT launcher on Windows or Linux.", ); } - preflightCodexIntegration(config, { - replaceExistingRoute: options.replaceCodexRoute, - }); + if (manageCodexRoute) { + preflightCodexIntegration(config, { + replaceExistingRoute: options.replaceCodexRoute, + }); + } const refreshTunnelWorker = tunnelWorkerRuntimeChanged(existing, config); if (existing && options.restartService) config.controlToken = randomBytes(32).toString("base64url"); const beforeService = getServiceStatus(); @@ -379,9 +390,11 @@ export async function setup(options: SetupOptions): Promise { launcherOwned && existing && existing.browserHost !== "launcher", ); if (!migratingTerminalRuntime) removeLegacyRuntimeArtifacts(config); - installCodexIntegration(config, { - replaceExistingRoute: options.replaceCodexRoute, - }); + if (manageCodexRoute) { + installCodexIntegration(config, { + replaceExistingRoute: options.replaceCodexRoute, + }); + } return { mode: config.mode, @@ -389,7 +402,7 @@ export async function setup(options: SetupOptions): Promise { loginCreated, serviceLoaded: launcherOwned ? false : getServiceStatus().loaded, tunnelReady, - codexRestartRequired: true, + codexRestartRequired: manageCodexRoute, connectorSetupRequired: config.mode === "full", }; } diff --git a/src/version.ts b/src/version.ts index 31fe0b02e..a77b29a7b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = "1.0.1-ko.3"; +export const VERSION = "1.0.1-ko.4"; diff --git a/tests/setup-lifecycle.test.ts b/tests/setup-lifecycle.test.ts index a7d87bff3..78b517139 100644 --- a/tests/setup-lifecycle.test.ts +++ b/tests/setup-lifecycle.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { launcherCapabilityProbeRequired, setupProxyIsReady } from "../src/setup"; +import { launcherCapabilityProbeRequired, setupManagesCodexRoute, setupProxyIsReady } from "../src/setup"; const config = { mode: "browser-only" as const, @@ -36,3 +36,12 @@ test("repeat launcher setup reuses the previously verified Pro capability", () = proAvailable: true, } as never)).toBe(true); }); + +test("full MCP setup can preserve an existing Codex route without accepting replacement", () => { + expect(setupManagesCodexRoute({ preserveCodexRoute: true })).toBe(false); + expect(setupManagesCodexRoute({ replaceCodexRoute: false })).toBe(true); + expect(() => setupManagesCodexRoute({ + replaceCodexRoute: true, + preserveCodexRoute: true, + })).toThrow("Choose either --replace-codex-route or --preserve-codex-route"); +});