Repository navigation
fix(codex): close the silent paths into a Codex lockout #5288
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
70bcd62
f90d94c
006118e
f0b84ab
b6fc33b
6982340
61a938f
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,90 @@ | ||
| # Lane H2 — the remaining Codex lockout dead ends (#5261) | ||
|
|
||
| Status: OPEN. Base is `origin/dev` at `043aa435ff`, after lane H landed as `9880c3cad2`. | ||
| One branch, ordered commits, one pull request to `dev`. | ||
|
|
||
| Lane H removed the dead end a locked-out user hits: injected routing now names its own undo, | ||
| a dead proxy is told to say so, and there is a troubleshooting page. This lane takes the four | ||
| paths INTO that state which lane H listed as not closed. | ||
|
|
||
| ## Why these four belong together | ||
|
|
||
| None of them locks anyone out alone. Each one removes a signal, and the incident is what | ||
| happens when all of them are missing at once: a proxy that stopped, an autostart that failed | ||
| silently, a reboot that restarted nothing, a catalog pointer left naming a deleted file, and an | ||
| account flow that looked like it was working. Every individual step had an explanation; the | ||
| user had no way to reach any of them. | ||
|
|
||
| ## 1. The shim hid autostart failure, and could prevent Codex launching | ||
|
|
||
| The wrapper ran `ocx ensure` with both streams discarded and `|| true`, then launched Codex | ||
| regardless (`src/codex/shim-templates.ts:139`). A failed start was invisible. | ||
|
|
||
| Ensure's own streams stay discarded rather than being let through. It prints progress and | ||
| warnings on exit-zero runs too, so a wrapper that leaked them would put noise in front of every | ||
| ordinary launch, which is how a diagnostic gets ignored. The exit status is the signal. | ||
|
|
||
| PowerShell had the opposite defect in the same place: a throwing `ensure` escaped a `try/finally` | ||
| with no `catch`, so Codex never launched at all. The autostart helper was producing the exact | ||
| lockout it exists to prevent, and the previous test asserted that propagation as correct | ||
| behaviour. It now catches, reports, and hands over. | ||
|
|
||
| Not closed: the Unix revision marker moved to 3 so installed Unix shims regenerate, but Windows | ||
| shims carry no revision marker and are excluded from obsolete-shim refresh | ||
| (`src/codex/shim.ts:892`). Existing Windows wrappers keep the old text until reinstalled. Giving | ||
| Windows a refresh path is its own change. | ||
|
|
||
| ## 2. The catalog pointer — my lane H note was wrong | ||
|
|
||
| Lane H recorded that nothing revalidates `model_catalog_json` after injection. That is not true. | ||
| The chooser refuses a missing owned path and the caller strips the stale line | ||
| (`src/codex/inject.ts:327`), with end-to-end coverage already in place. | ||
|
|
||
| The real gap is narrower and worse: that repair only reaches someone who runs opencodex again, | ||
| and the difficulty of this state is that Codex is the thing that stopped working, so nothing | ||
| prompts them to. A `model_catalog_json` naming a file that is gone does not degrade Codex, it | ||
| stops Codex loading its configuration at all — the same blank wall as dead routing, from a | ||
| different cause. So this lane adds detection, not repair, and `ocx status` now names the file | ||
| and both ways out. | ||
|
|
||
| ## 3. Reboot: stated, not faked | ||
|
|
||
| Applying the integration does not install a service, and the Windows scheduled task a separate | ||
| install would create is logon-triggered (`src/service/windows-taskxml.ts:225`). Setup ended on a | ||
| success line without ever saying routing outlives the proxy. | ||
|
|
||
| A BootTrigger is deliberately NOT the fix. The task runs as the interactive user | ||
| (`LogonType: InteractiveToken`), so before logon there is no session for it to run in; the | ||
| trigger would read like a fix and change nothing. Genuine pre-logon start means a different | ||
| principal and a different service backend, which is larger than this lane and is left open | ||
| rather than half-done. What was cheap and true on every platform is saying the dependency | ||
| exists, reusing the health model `ocx status` and `ocx doctor` already report. | ||
|
|
||
| ## 4. Account pool: a silence, not an error | ||
|
|
||
| The URL launcher swallowed its own failure and returned nothing, so the Codex login route | ||
| answered identically whether a browser opened, failed, or was skipped. The CLI printed the URL | ||
| and polled, and a user whose machine could not launch a browser watched something that looked | ||
| like it was working. | ||
|
|
||
| Launch failure is now reported and never fatal — the URL is still worth opening by hand and the | ||
| flow stays live. The CLI names the fixed callback port 1455, because ChatGPT supplies the | ||
| redirect URI and the flow cannot move to a free port, so `--device` is the way around it. | ||
|
|
||
| Not closed: the dashboard keeps last-good rows after a failed refresh | ||
| (`gui/src/hooks/useCodexAccountPool.ts:325`), which is why a newly added account can be absent | ||
| while older rows still show. That is a GUI change, and the pull-request gate requires a | ||
| screenshot of a UI change, which cannot be produced under this lane's no-build constraint. It | ||
| is left for a lane that can build the GUI. | ||
|
|
||
| ## Verification | ||
|
|
||
| Static source review and exact-head hosted CI only. NOT RUN, by lane constraint: local suite, | ||
| individual tests, typecheck, build, install, `ocx` execution, service start or restart, and any | ||
| change to credentials or configuration on the machine. The incident being fixed is a | ||
| configuration change that locked a user out. | ||
|
|
||
| Checked before push: the file-size ratchet (no tracked file over cap; `codex-shim.test.ts` | ||
| sits close to its cap, so the new shim tests went to a sibling file registered in both | ||
| layout maps), and the restated-constant class that broke two lanes in this batch — the shim | ||
| test's private literal copies of the marker constants now come from the source module. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { durableBunRuntime } from "../lib/bun-runtime"; | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config"; | ||
| import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state"; | ||
| import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor"; | ||
|
|
@@ -7,6 +8,8 @@ import type { OcxConfig } from "../types"; | |
| import { diagnoseService, serviceLogPath } from "../service"; | ||
| import { collectStartupHealth, type StartupHealth } from "../codex/autostart-health"; | ||
| import { getCodexRoutingKind } from "../codex/inject"; | ||
| import { missingOwnedCatalogPath } from "../codex/inject/config-toml"; | ||
| import { CODEX_CONFIG_PATH } from "../codex/paths"; | ||
| import { diagnoseCodexShim } from "../codex/shim"; | ||
| import { displayCodexRuntimePath, effortClampAppliesToRuntime, liveRemovedEfforts, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime"; | ||
| import { packageVersion } from "./help"; | ||
|
|
@@ -529,6 +532,38 @@ export function deadProxyRoutingAdviceLines(input: { | |
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Read the live Codex config and report an opencodex catalog pointer whose file is gone. | ||
| * | ||
| * Unreadable or absent config is reported as no finding rather than as a problem: this is a | ||
| * diagnostic line, and inventing one from missing evidence is worse than staying quiet. | ||
| */ | ||
| export function detectMissingCodexCatalogPath(): string | null { | ||
| try { | ||
| if (!existsSync(CODEX_CONFIG_PATH)) return null; | ||
| return missingOwnedCatalogPath(readFileSync(CODEX_CONFIG_PATH, "utf8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The one state in this report where Codex is broken independently of the proxy (#5261). | ||
| * | ||
| * A `model_catalog_json` naming a file that is gone stops Codex loading its configuration at | ||
| * all, so it presents as the same blank wall as dead routing while having a different cause and | ||
| * a different fix. Both are named, because restarting the proxy rewrites the catalog and | ||
| * restoring removes the pointer, and which one the user wants is their choice, not ours. | ||
| */ | ||
| export function missingCodexCatalogLines(missingCatalogPath: string | null): string[] { | ||
| if (!missingCatalogPath) return []; | ||
| return [ | ||
| "⚠️ Codex is pointed at a model catalog that is no longer on disk, so Codex cannot load its config:", | ||
| ` ${missingCatalogPath}`, | ||
| " Regenerate it with 'ocx start', or remove opencodex from Codex with 'ocx restore'.", | ||
|
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 catalog is deleted while the proxy is already running, Useful? React with 👍 / 👎. |
||
| ]; | ||
| } | ||
|
|
||
| export async function collectStatus(): Promise<CliStatusView> { | ||
| const configDiagnostics = readConfigDiagnostics(); | ||
| const config = configDiagnostics.config; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -226,9 +226,12 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig, | |
| // machine. Opening it on the hub host is useless at best, and on a | ||
| // headless host it fails. `deviceCode` is the same signal the generic | ||
| // OAuth login route uses to make this decision. | ||
| // Reported to the caller rather than discarded (#5261): a login whose browser never opened | ||
| // is indistinguishable from one that did, so it reads as success while nothing happens. | ||
| let browserLaunch: "started" | "failed" | "skipped" = "skipped"; | ||
| if (result.url && !result.deviceCode && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) { | ||
| const { openUrl } = await import("../../lib/open-url"); | ||
| openUrl(result.url); | ||
| browserLaunch = (await openUrl(result.url)).status === "started" ? "started" : "failed"; | ||
|
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.
This commit changes authentication responses, browser-launch behavior, Codex config diagnostics, shim failure semantics, and CLI setup/status output across AGENTS.md reference: src/AGENTS.md:L11-L11 Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| (async () => { | ||
|
|
@@ -488,6 +491,8 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig, | |
| flowId, | ||
| url: result.url, | ||
| instructions: result.instructions, | ||
| // Never fatal: the URL is still a valid thing to open by hand, and the flow stays live. | ||
| browserLaunch, | ||
| // Dropped before #3366: every device-code surface renders this field, | ||
| // so withholding it left the GUI and CLI with no code to show. | ||
| ...(result.deviceCode ? { deviceCode: result.deviceCode } : {}), | ||
|
|
||
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 a first-time user declines Codex injection but their existing Codex config points at a custom-local or unclassifiable local route,
collectStartupHealthstill returnsat-risk, so setup claims that routing was written by this setup and recommendsocx restoreeven though opencodex did not install that route. Track whether injection actually succeeded or requirehealth.routingInjectedbefore printing this opencodex-specific warning.Useful? React with 👍 / 👎.