Skip to content
Closed
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
8 changes: 8 additions & 0 deletions docs-site/src/content/docs/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,即可在进程级别关闭自动恢复。

| 子命令 | 操作 |
Expand Down
69 changes: 69 additions & 0 deletions src/cli/codex-shim-readiness.ts
Original file line number Diff line number Diff line change
@@ -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") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prioritize external providers selected through profiles

When config.toml selects an external provider through profile = "work" and [profiles.work].model_provider, currentExternalCodexModelProvider() returns that provider, but getCodexRoutingKind() only classifies root routing and can return native or opencodex-local; this function consequently returns no warning. The shim's subsequent ensure also preserves that external route, so installation appears green even though Codex requests bypass OpenCodex. Check externalProvider independently before accepting a routing kind as ready, and add a focused profile-selector regression test.

AGENTS.md reference: AGENTS.md:L228-L230

Useful? React with 👍 / 👎.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the advertised readiness check read-only

loadConfig() is a mutating loader: it hardens directory and secret permissions and, when an invalid configuration cannot be repaired, writes a timestamped .invalid-* backup. Consequently, every fresh ocx codex-shim install process can create another copy of an invalid configuration—including any credentials it contains—even though the newly added lifecycle documentation says these readiness checks are read-only. Use the existing observe-only readConfigDiagnostics() path to inspect config.proxy without altering the user's configuration state.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

return codexShimReadinessWarnings({
routingKind: getCodexRoutingKind(),
externalProvider: currentExternalCodexModelProvider(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Catch readiness probe failures after installing the shim

If config.toml exists but cannot be read, is replaced between the existence check and read, or is a directory, currentExternalCodexModelProvider() throws because unlike getCodexRoutingKind() it does not catch readFileSync failures. This happens after installCodexShim() has already modified the launcher, so an advisory probe turns a successful installation into an uncaught non-zero CLI failure without printing the install result. Treat probe errors as unverifiable routing and emit the warning instead of allowing them to escape.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check project-local provider overrides before reporting ready

When installation runs inside, or globally trusts, a repository whose .codex/config.toml selects an external provider, these probes inspect only the global CODEX_CONFIG_PATH, so a native or OpenCodex-managed global route produces no warning even though Codex's merged project configuration bypasses the proxy. The repository already models this concrete override in collectProjectCodexConfigWarnings() and analyzeProjectCodexConfig(); include those diagnostics in readiness and add a focused project-config regression test before printing green.

AGENTS.md reference: src/AGENTS.md:L24-L26

Useful? React with 👍 / 👎.

processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Warn when config.proxy depends on the current environment

When config.proxy is an environment reference such as ${HTTPS_PROXY}, this check treats it as persistent merely because it resolves in the installer process. If Codex is later launched from a desktop session or another shell that does not inherit that variable, the shim-started proxy loads the same reference as unresolved and loses outbound connectivity, yet installation was reported green; generated background-service environments likewise do not preserve arbitrary proxy variables. Distinguish a literal persisted proxy from an environment reference whose value exists only now, and add focused coverage for this handoff case.

AGENTS.md reference: AGENTS.md:L228-L230

Useful? React with 👍 / 👎.

});
Comment on lines +61 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep readiness collection advisory when Codex config reads fail.

Line 64 fails closed to "unknown" when Codex config cannot be read. Line 65 then calls currentExternalCodexModelProvider(), whose upstream implementation reads the same file without a catch. If the file is unreadable or changes between the existence check and read, codex-shim install throws instead of reporting the unresolved-routing warning.

Catch this read failure and use externalProvider: null. Add a regression test for an unreadable or removed Codex config.

Proposed fix
 export function collectCodexShimReadinessWarnings(): string[] {
   const config = loadConfig();
+  const routingKind = getCodexRoutingKind();
+  let externalProvider: string | null = null;
+  try {
+    externalProvider = currentExternalCodexModelProvider();
+  } catch {
+    // Routing is already classified as unknown when config cannot be read.
+  }
+
   return codexShimReadinessWarnings({
-    routingKind: getCodexRoutingKind(),
-    externalProvider: currentExternalCodexModelProvider(),
+    routingKind,
+    externalProvider,
     processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
     configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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()),
});
export function collectCodexShimReadinessWarnings(): string[] {
const config = loadConfig();
const routingKind = getCodexRoutingKind();
let externalProvider: string | null = null;
try {
externalProvider = currentExternalCodexModelProvider();
} catch {
// Routing is already classified as unknown when config cannot be read.
}
return codexShimReadinessWarnings({
routingKind,
externalProvider,
processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/codex-shim-readiness.ts` around lines 61 - 68, Update
collectCodexShimReadinessWarnings to catch failures from
currentExternalCodexModelProvider and pass externalProvider: null so readiness
collection remains advisory when the Codex config is unreadable or removed. Add
a regression test covering that read failure and verify the unresolved-routing
warning is returned instead of throwing.

}
9 changes: 7 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
144 changes: 144 additions & 0 deletions tests/codex-shim-readiness.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
});
});
Loading