From d8968b7e6ea4bae2c611cb776a9329f077a00016 Mon Sep 17 00:00:00 2001 From: TyroneXie <328347833@qq.com> Date: Fri, 7 Aug 2026 11:07:38 +0800 Subject: [PATCH] fix(codex): warn when codex-shim install cannot prove routing --- .../content/docs/reference/cli/lifecycle.md | 8 + .../docs/zh-cn/reference/cli/lifecycle.md | 2 + src/cli/codex-shim-readiness.ts | 69 +++++++++ src/cli/index.ts | 9 +- tests/codex-shim-readiness.test.ts | 144 ++++++++++++++++++ 5 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/cli/codex-shim-readiness.ts create mode 100644 tests/codex-shim-readiness.test.ts diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index ef1f0f47a..827095d8b 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -281,6 +281,14 @@ dashboard UAC prompt or rerun `ocx service install` in an elevated PowerShell wi Wrap a script-based `codex` launcher on PATH with a lightweight autostart script. Real `codex.exe` targets are left untouched to avoid breaking exact executable invocations. +Launcher installation alone does not prove that Codex requests will use OpenCodex. After a healthy +install, the command checks the current Codex routing and reports a warning instead of a green result +when routing is external, user-owned, or unverifiable. It also warns when outbound proxy variables +exist only in the current process while `config.proxy` is unset or unresolved, because Codex +launchers and background services may not inherit that environment. These checks are read-only and +never print proxy values; resolve the reported handoff and run `ocx doctor` before relying on +autostart. + If a completed external Codex update overwrites an installed shim, the next ordinary `ocx` command backs up the stable new launcher and restores the shim before dispatch. A launcher that is still changing is left untouched and retried later. Repair failures warn without failing the requested diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index 124a36c05..083ab7238 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -176,6 +176,8 @@ ocx service uninstall 在 PATH 上把基于脚本的 `codex` 启动器包装为一个轻量自启动脚本。真实的 `codex.exe` 目标会保持不变,以避免破坏精确的可执行文件调用。 +仅安装启动器并不能证明 Codex 请求会经过 OpenCodex。完成健康安装后,命令会检查当前 Codex 路由;当路由由外部配置、用户自有网关管理或无法验证时,会显示警告而不是绿色成功。若出站代理变量只存在于当前进程,而 `config.proxy` 未设置或无法解析,也会给出警告,因为 Codex 启动器和后台服务未必继承该环境。这些检查只读且绝不会打印代理值;在依赖自动启动前,请先处理提示的交接配置并运行 `ocx doctor`。 + 如果已完成的外部 Codex 更新覆盖了已安装的 shim,下一次普通的 `ocx` 命令会先备份稳定的新启动器,再在分发前恢复 shim。仍在变动中的启动器会保持不动,并在稍后重试。修复失败只会警告,不会让所请求的命令失败;手动回退:`ocx codex-shim install`。将 `codexShimAutoRestore` 设为 `false`,或设置 `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`,即可在进程级别关闭自动恢复。 | 子命令 | 操作 | diff --git a/src/cli/codex-shim-readiness.ts b/src/cli/codex-shim-readiness.ts new file mode 100644 index 000000000..e8db24c6f --- /dev/null +++ b/src/cli/codex-shim-readiness.ts @@ -0,0 +1,69 @@ +import { + currentExternalCodexModelProvider, + getCodexRoutingKind, + type CodexRoutingKind, +} from "../codex/inject"; +import { loadConfig, resolveEnvValue } from "../config"; + +const PROXY_ENV_KEYS = [ + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", +] as const; + +export interface CodexShimReadinessInputs { + routingKind: CodexRoutingKind; + externalProvider: string | null; + processProxyEnvPresent: boolean; + configuredProxyResolved: boolean; +} + +function externalProviderLabel(provider: string | null): string { + return provider + ? `external model_provider ${JSON.stringify(provider)}` + : "the active Codex route"; +} + +export function codexShimReadinessWarnings( + inputs: CodexShimReadinessInputs, +): string[] { + const warnings: string[] = []; + const provider = externalProviderLabel(inputs.externalProvider); + + if (inputs.routingKind === "unknown") { + warnings.push( + inputs.externalProvider + ? `Codex still selects ${provider}. The shim can start OpenCodex, but it does not redirect that provider; point it at the live OpenCodex /v1 endpoint with wire_api = "responses", or switch to the built-in openai provider and run 'ocx sync'.` + : "Codex routing could not be verified. The shim can start OpenCodex, but it may not redirect Codex; run 'ocx doctor' before relying on autostart.", + ); + } else if (inputs.routingKind === "custom-local") { + warnings.push( + `Codex uses ${provider} through a user-owned local gateway. The shim can start OpenCodex, but OpenCodex does not own that route; run 'ocx doctor' to verify its lifecycle.`, + ); + } else if (inputs.routingKind === "custom-remote") { + warnings.push( + `Codex uses ${provider} through a remote gateway. The shim only starts a local OpenCodex proxy and will not affect those requests.`, + ); + } + + if (inputs.processProxyEnvPresent && !inputs.configuredProxyResolved) { + warnings.push( + "Proxy environment variables are present only in this process while config.proxy is unset or unresolved. Codex launchers and background services may not inherit them; persist config.proxy before relying on autostart.", + ); + } + + return warnings; +} + +export function collectCodexShimReadinessWarnings(): string[] { + const config = loadConfig(); + return codexShimReadinessWarnings({ + routingKind: getCodexRoutingKind(), + externalProvider: currentExternalCodexModelProvider(), + processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())), + configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()), + }); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index e9f3c22c3..a7e67d565 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1033,11 +1033,16 @@ switch (command) { break; } case "codex-shim": { - const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim"); + const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim"); switch (args[1]) { case "install": { const r = installCodexShim(); - console.log(r.installed ? `✅ ${r.message}` : `⚠️ ${r.message}`); + const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness"); + const warnings = diagnoseCodexShim().healthy + ? collectCodexShimReadinessWarnings() + : []; + console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`); + for (const warning of warnings) console.warn(` ${warning}`); break; } case "status": diff --git a/tests/codex-shim-readiness.test.ts b/tests/codex-shim-readiness.test.ts new file mode 100644 index 000000000..7c004aef9 --- /dev/null +++ b/tests/codex-shim-readiness.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { codexShimReadinessWarnings } from "../src/cli/codex-shim-readiness"; + +const repoRoot = dirname(fileURLToPath(new URL("../package.json", import.meta.url))); +const cliPath = join(repoRoot, "src", "cli", "index.ts"); + +const ready = { + routingKind: "native" as const, + externalProvider: null, + processProxyEnvPresent: false, + configuredProxyResolved: false, +}; + +describe("Codex shim install readiness", () => { + test("keeps a clean install green for native and managed routing", () => { + expect(codexShimReadinessWarnings(ready)).toEqual([]); + expect(codexShimReadinessWarnings({ + ...ready, + routingKind: "opencodex-local", + })).toEqual([]); + }); + + test("warns when an external provider is not routed through OpenCodex", () => { + const warnings = codexShimReadinessWarnings({ + ...ready, + routingKind: "unknown", + externalProvider: "custom", + }); + + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain('external model_provider "custom"'); + expect(warnings[0]).toContain("live OpenCodex /v1 endpoint"); + expect(warnings[0]).toContain('wire_api = "responses"'); + + }); + + test("distinguishes user-owned local and remote routes", () => { + const local = codexShimReadinessWarnings({ + ...ready, + routingKind: "custom-local", + externalProvider: "gateway", + }); + expect(local).toHaveLength(1); + expect(local[0]).toContain("user-owned local gateway"); + expect(local[0]).toContain("ocx doctor"); + + const remote = codexShimReadinessWarnings({ + ...ready, + routingKind: "custom-remote", + externalProvider: "gateway", + }); + expect(remote).toHaveLength(1); + expect(remote[0]).toContain("remote gateway"); + expect(remote[0]).toContain("will not affect those requests"); + }); + + test("warns about process-only proxy settings without exposing a URL", () => { + const warnings = codexShimReadinessWarnings({ + ...ready, + processProxyEnvPresent: true, + configuredProxyResolved: false, + }); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain("config.proxy"); + expect(warnings[0]).toContain("may not inherit"); + expect(warnings[0]).not.toContain("://"); + + expect(codexShimReadinessWarnings({ + ...ready, + processProxyEnvPresent: true, + configuredProxyResolved: true, + })).toEqual([]); + }); + + test("the install command surfaces readiness warnings without leaking the proxy URL", () => { + if (process.platform === "win32") return; + + const root = mkdtempSync(join(tmpdir(), "ocx-shim-readiness-")); + const codexHome = join(root, "codex-home"); + const opencodexHome = join(root, "opencodex-home"); + const binDir = join(root, "bin"); + mkdirSync(codexHome); + mkdirSync(opencodexHome); + mkdirSync(binDir); + try { + writeFileSync(join(codexHome, "config.toml"), [ + 'model_provider = "custom"', + "", + "[model_providers.custom]", + 'name = "OpenAI"', + 'wire_api = "responses"', + "", + ].join("\n"), "utf8"); + writeFileSync(join(opencodexHome, "config.json"), `${JSON.stringify({ + port: 10100, + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + }, + }, + defaultProvider: "openai", + }, null, 2)}\n`, "utf8"); + const codex = join(binDir, "codex"); + writeFileSync(codex, "#!/bin/sh\nexit 0\n", "utf8"); + chmodSync(codex, 0o755); + + const proxyUrl = "http://user:secret@127.0.0.1:7890"; + const result = spawnSync(process.execPath, [cliPath, "codex-shim", "install"], { + cwd: repoRoot, + env: { + ...process.env, + CODEX_HOME: codexHome, + OPENCODEX_HOME: opencodexHome, + PATH: `${binDir}:${process.env.PATH ?? ""}`, + HTTP_PROXY: proxyUrl, + HTTPS_PROXY: proxyUrl, + }, + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toStartWith("⚠️ Codex autostart shim installed"); + expect(result.stderr).toContain('external model_provider "custom"'); + expect(result.stderr).toContain("config.proxy"); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(proxyUrl); + expect(`${result.stdout}\n${result.stderr}`).not.toContain("user:secret"); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +});