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
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 @@ -287,6 +287,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 @@ -178,6 +178,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") {
warnings.push(
inputs.externalProvider
Comment on lines +36 to +38

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 Treat profile-selected providers as external

When config.toml selects a provider through profile = "work" and [profiles.work] model_provider = "anthropic", currentExternalCodexModelProvider() correctly returns anthropic, but getCodexRoutingKind() ignores profile sections and returns native. Because this branch only uses externalProvider for an unknown route, codex-shim install still prints a green result even though Codex bypasses OpenCodex. Warn whenever externalProvider is non-null, or classify the effective profile route, and add a profile-based regression case.

Useful? React with 👍 / 👎.

? `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(),
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 Keep readiness probing advisory on unreadable config

If the Codex config.toml exists but cannot be read—for example because of permissions or a transient Windows sharing error—getCodexRoutingKind() degrades to unknown, but this subsequent call rereads the same file without a catch and throws. The shim may already have been installed successfully, yet this new advisory check makes the command exit nonzero with an uncaught error. Catch this probe failure and report the unverifiable-routing warning instead.

Useful? React with 👍 / 👎.

processProxyEnvPresent: PROXY_ENV_KEYS.some(key => Boolean(process.env[key]?.trim())),
configuredProxyResolved: Boolean(resolveEnvValue(config.proxy)?.trim()),
Comment on lines +66 to +67

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 proxy config resolves only through this process

When config.proxy is an environment reference such as ${HTTP_PROXY}, running install from a shell where that variable is set makes configuredProxyResolved true and suppresses the warning. A later Codex launch from launchd, a desktop app, or another environment may not contain that variable, leaving config.proxy unresolved and outbound requests without the required proxy—the exact handoff risk this check is intended to expose. Distinguish a persisted literal proxy from a value that resolves only through the current process, or warn for environment-backed proxy configuration.

Useful? React with 👍 / 👎.

});
}
9 changes: 7 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,11 +1160,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()
: [];
Comment on lines +1167 to +1170

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether loadConfig throws on unreadable/corrupt config and whether the reader is guarded.
set -euo pipefail

ast-grep run --pattern 'export function loadConfig($$$) { $$$ }' --lang typescript src/config.ts | head -80

rg -nP -C4 '\b(readFileSync|JSON\.parse|throw)\b' src/config.ts | rg -n -C4 'loadConfig' || true

# Check whether any other CLI call site already wraps this readiness helper.
rg -nP -C4 'collectCodexShimReadinessWarnings' src tests

Repository: lidge-jun/opencodex

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -u

printf 'Repo files count: '
git ls-files | wc -l

printf '\nLocate relevant files:\n'
fd -a 'index\.ts|codex-shim-readiness\.ts|config\.ts|inject\.ts|lifecycle\.md' . | sed 's#^\./##'

printf '\nSearch relevant symbols broadly:\n'
rg -n 'collectCodexShimReadinessWarnings|loadConfig|currentExternalCodexModelProvider|diagnoseCodexShim|installCodexShim|readConfig|config.json' . --glob '!node_modules' --glob '!dist' --glob '!build' || true

printf '\ngit status/stat:\n'
git status --short
git diff --stat || true

Repository: lidge-jun/opencodex

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -u

printf 'src/cli/index.ts install section:\n'
sed -n '1145,1182p' src/cli/index.ts | cat -n

printf '\nsrc/cli/codex-shim-readiness.ts:\n'
sed -n '1,120p' src/cli/codex-shim-readiness.ts | cat -n

printf '\nsrc/codex/inject.ts relevant external provider functions:\n'
rg -n -C5 'currentExternalCodexModelProvider|shouldInjectApiAuthHeader|getCodexRoutingKind' src/codex/inject.ts

printf '\nRelevant lifecycle doc excerpt:\n'
sed -n '276,302p' docs-site/src/content/docs/reference/cli/lifecycle.md | cat -n

Repository: lidge-jun/opencodex

Length of output: 11723


🏁 Script executed:

#!/bin/bash
set -u

printf 'src/config.ts loadConfig summary area:\n'
sed -n '924,1045p' src/config.ts | cat -n

printf '\nconfig migration/read-error handling tests around loadConfig failures:\n'
sed -n '160,190p' tests/config.test.ts | cat -n
sed -n '853,880p' tests/config.test.ts | cat -n
sed -n '976,1000p' tests/config.test.ts | cat -n

printf '\nCodex inject relevant reads around currentExternalCodexModelProvider/getCodexRoutingKind:\n'
sed -n '68,92p' src/codex/inject.ts | cat -n
sed -n '378,398p' src/codex/inject.ts | cat -n

printf '\nBehavioral probe of currentExternalCodexModelProvider pattern:\n'
node - <<'JS'
const fs = require('fs');
const path = '/tmp/opencodex-codex-readiness-probe-toml';
for (const content of ['x_model_provider = "abc"\n', '[model]\nx_model_provider = "abc"\n']) {
  fs.writeFileSync(path, content, 'utf8');
  const exists = fs.existsSync(path);
  try {
    const actual = exists ? fs.readFileSync(path, 'utf8') : null;
    console.log('before content=', content.trim(), 'exists=', exists, 'read=', actual.trim());
  } catch (error) {
    console.log('before content=', content.trim(), 'exists=', exists, 'read_threw=', error.code);
  }
  try {
    fs.unlinkSync(path);
  } catch {}
}
JS

Repository: lidge-jun/opencodex

Length of output: 13349


Guard the readiness probe on successful shim install.

src/cli/index.ts:1168-1169 calls collectCodexShimReadinessWarnings() directly after installCodexShim() succeeds. That helper calls loadConfig() and currentExternalCodexModelProvider(), and currentExternalCodexModelProvider() can throw between existsSync(CODEX_CONFIG_PATH) and readFileSync(CODEX_CONFIG_PATH) when the file is removed or becomes unreadable. As written, an advisory/read-only check can abort the ocx codex-shim install case and exit non-zero after the shim was installed, contradicting the advisory, read-only behavior documented in docs-site/src/content/docs/reference/cli/lifecycle.md. Wrap this call in a fail-open try/catch so install returns zero and converts probe failures to non-blocking warnings.

🤖 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/index.ts` around lines 1167 - 1170, Wrap the readiness probe call in
the codex-shim install flow, specifically around
collectCodexShimReadinessWarnings(), with a fail-open try/catch. Preserve
successful warning collection, but convert any probe failure into a non-blocking
warning so installCodexShim() still completes with exit status zero.

console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
Comment on lines +1168 to +1171

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Report an unhealthy shim instead of discarding the diagnostic verdict.

diagnoseCodexShim() can return installed: true with healthy: false (src/codex/shim.ts lines 1179-1211: a missing wrapper, a missing backup, or a wrapper that is not an OpenCodex shim). In that case line 1170 sets warnings to [], and line 1171 prints because r.installed is true and warnings.length === 0. The command therefore reports a green result for the exact state the new diagnostic was added to detect.

Suppress the green marker when the diagnostic is not healthy, and surface the diagnostic summary.

🐛 Proposed fix: honor the unhealthy verdict
-        const warnings = diagnoseCodexShim().healthy
-          ? collectCodexShimReadinessWarnings()
-          : [];
-        console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️  "}${r.message}`);
+        const diagnostic = diagnoseCodexShim();
+        const warnings = diagnostic.healthy
+          ? collectCodexShimReadinessWarnings()
+          : [diagnostic.summary];
+        console.log(`${r.installed && diagnostic.healthy && warnings.length === 0 ? "✅ " : "⚠️  "}${r.message}`);
📝 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
const warnings = diagnoseCodexShim().healthy
? collectCodexShimReadinessWarnings()
: [];
console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
const diagnostic = diagnoseCodexShim();
const warnings = diagnostic.healthy
? collectCodexShimReadinessWarnings()
: [diagnostic.summary];
console.log(`${r.installed && diagnostic.healthy && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
🤖 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/index.ts` around lines 1168 - 1171, Update the Codex shim reporting
around diagnoseCodexShim so an installed but unhealthy diagnostic cannot produce
a green marker. Preserve readiness warnings for healthy shims, include the
diagnostic summary when healthy is false, and base the status icon on both
r.installed and the diagnostic health/warnings outcome.

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;

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Bun test runner test.skipIf conditional skip API

💡 Result:

In Bun's test runner, test.skipIf() is a modifier used to conditionally skip a test based on a provided boolean condition [1]. Usage The test.skipIf(condition) method accepts a boolean value [2]. If the condition evaluates to true, the test will be skipped [2][3]. Example ts import { test } from "bun:test"; const isMacOS = process.platform === "darwin"; test.skipIf(isMacOS)("only runs on non-macOS platforms", () => { // This test will be skipped if the platform is macOS }); Key Details - Scope: It can be used directly on individual tests [1] or on describe blocks as describe.skipIf() to skip groups of tests [4][5]. - Complementary API: If you want to run a test only when a condition is met (the opposite of skipping), you can use .if() (e.g., test.if(condition)(...)) [2][1]. - Related Modifiers: Other similar conditional modifiers exist, such as todoIf(condition), which marks a test as "to-do" only if the condition is met [3][6]. Documentation for these APIs can be found in the bun:test module reference [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant file outline:"
ast-grep outline tests/codex-shim-readiness.test.ts --view compact || true

echo
echo "Relevant test section:"
nl -ba tests/codex-shim-readiness.test.ts | sed -n '70,110p'

echo
echo "Other early Windows returns and skipIf usage in tests:"
rg -n 'process\.platform === "win32"(?!\s*&&)|\.skipIf\(|\.only\(' tests || true

Repository: lidge-jun/opencodex

Length of output: 395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant test section:"
awk '{printf "%6d\t%s\n", NR, $0}' tests/codex-shim-readiness.test.ts | sed -n '70,110p'

echo
echo "Other early Windows returns and skipIf usage in tests:"
rg -n 'process\.platform === "win32"(?!\s*&&)|\.skipIf\(|\.only\(' tests || true

Repository: lidge-jun/opencodex

Length of output: 2195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Early Windows returns:"
grep -n 'process\.platform === "win32"' tests || true

echo
echo "Other skipIf/only usage in tests:"
grep -n '\.skipIf(\|\.only(' tests || true

Repository: lidge-jun/opencodex

Length of output: 271


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Early Windows returns:"
grep -rn 'process\.platform === "win32"' tests || true

echo
echo "Other skipIf/only usage in tests:"
grep -rn '\.skipIf(\|\.only(' tests || true

echo
echo "Full test around issue:"
awk '{printf "%6d\t%s\n", NR, $0}' tests/codex-shim-readiness.test.ts | sed -n '84,145p'

Repository: lidge-jun/opencodex

Length of output: 11163


Use test.skipIf for the Windows-only guard.

At tests/codex-shim-readiness.test.ts:87, if (process.platform === "win32") return; exits before the install command is invoked and before any assertions run. This leaves the Windows run reporting a passing test while skipping coverage of this Windows-incompatible setup. Use test.skipIf(process.platform === "win32")(...) so Bun records the test as skipped.

🤖 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 `@tests/codex-shim-readiness.test.ts` at line 87, Replace the early Windows
return in the readiness test with Bun’s test.skipIf(process.platform ===
"win32") wrapper around the test definition, preserving the existing install
command and assertions while recording Windows runs as skipped.


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