-
Notifications
You must be signed in to change notification settings - Fork 691
fix(codex): warn when codex-shim install cannot prove routing (#1169) #1289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ? `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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the Codex 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 testsRepository: 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 || trueRepository: 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 -nRepository: 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 {}
}
JSRepository: lidge-jun/opencodex Length of output: 13349 Guard the readiness probe on successful shim install.
🤖 Prompt for AI Agents |
||||||||||||||||||||
| console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`); | ||||||||||||||||||||
|
Comment on lines
+1168
to
+1171
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| for (const warning of warnings) console.warn(` ${warning}`); | ||||||||||||||||||||
| break; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| case "status": | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In Bun's test runner, 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: 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 At 🤖 Prompt for AI Agents |
||
|
|
||
| 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 }); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
config.tomlselects a provider throughprofile = "work"and[profiles.work] model_provider = "anthropic",currentExternalCodexModelProvider()correctly returnsanthropic, butgetCodexRoutingKind()ignores profile sections and returnsnative. Because this branch only usesexternalProviderfor anunknownroute,codex-shim installstill prints a green result even though Codex bypasses OpenCodex. Warn wheneverexternalProvideris non-null, or classify the effective profile route, and add a profile-based regression case.Useful? React with 👍 / 👎.