Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions launcher/electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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", {
Expand All @@ -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") {
Expand Down
7 changes: 6 additions & 1 deletion launcher/electron/runtime.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -677,6 +681,7 @@ class RuntimeHost {
"--full",
"--browser-host-descriptor",
this.browserDescriptorPath,
"--preserve-codex-route",
];
if (reuseSavedCredentials) {
args.push("--acknowledge-unofficial", "--restart-service");
Expand Down Expand Up @@ -777,4 +782,4 @@ class RuntimeHost {
}
}

module.exports = { RuntimeHost };
module.exports = { RuntimeHost, launcherRuntimeAllowedForRoute };
2 changes: 1 addition & 1 deletion launcher/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
9 changes: 8 additions & 1 deletion launcher/tests/runtime-host.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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",
]);
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
8 changes: 7 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -137,6 +138,7 @@ async function setupCommand(args: string[]): Promise<void> {
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);

Expand Down Expand Up @@ -174,7 +176,11 @@ async function setupCommand(args: string[]): Promise<void> {
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<void> {
Expand Down
29 changes: 21 additions & 8 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface SetupOptions {
forceLogin?: boolean;
autoApproveToolCalls?: boolean;
replaceCodexRoute?: boolean;
preserveCodexRoute?: boolean;
restartService?: boolean;
acknowledgedUnofficial?: boolean;
tunnelId?: string;
Expand All @@ -53,7 +54,7 @@ export interface SetupResult {
loginCreated: boolean;
serviceLoaded: boolean;
tunnelReady: boolean | null;
codexRestartRequired: true;
codexRestartRequired: boolean;
connectorSetupRequired: boolean;
}

Expand All @@ -74,6 +75,13 @@ export function existingFullSetupCredentials(existing: AppConfig | undefined): E
};
}

export function setupManagesCodexRoute(options: Pick<SetupOptions, "replaceCodexRoute" | "preserveCodexRoute">): 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();
Expand Down Expand Up @@ -253,16 +261,19 @@ async function bootstrapTunnelProfile(config: AppConfig): Promise<void> {
export async function setup(options: SetupOptions): Promise<SetupResult> {
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(
"Terminal-only managed Chrome setup currently requires macOS. "
+ "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();
Expand Down Expand Up @@ -379,17 +390,19 @@ export async function setup(options: SetupOptions): Promise<SetupResult> {
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,
configPath: getConfigPath(),
loginCreated,
serviceLoaded: launcherOwned ? false : getServiceStatus().loaded,
tunnelReady,
codexRestartRequired: true,
codexRestartRequired: manageCodexRoute,
connectorSetupRequired: config.mode === "full",
};
}
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "1.0.1-ko.3";
export const VERSION = "1.0.1-ko.4";
11 changes: 10 additions & 1 deletion tests/setup-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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");
});