diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 890b83e7d..3dd10384d 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -95,6 +95,12 @@ checks for a single Windows Codex Desktop home at `/mnt/c/Users/*/.codex/config. one candidate exists, it uses that directory so WSL app-server mode and Windows Codex Desktop share the same config and auth files. Set `CODEX_HOME` explicitly to override this detection. +On Windows, an Orca shell can set both `CODEX_HOME` and `ORCA_CODEX_HOME` to Orca's bundled runtime +home while the ChatGPT/Codex app still reads `%USERPROFILE%\\.codex`. `ocx status` and `ocx doctor` +warn about this exact mismatch and print redacted target paths. If a background service was installed +from that Orca shell, uninstall it from the original shell first, then set `CODEX_HOME` to the app +home, unset `ORCA_CODEX_HOME`, rerun sync/restore, and install the service again. + In dedicated-provider mode, `requires_openai_auth = true` keeps Codex App/TUI account-gated surfaces aligned with native Codex. opencodex also serves `/v1/responses` over WebSocket. The dedicated provider advertises `supports_websockets = true` only when `"websockets": true`; on loopback Codex's diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index bfd0381f2..124a65232 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -94,6 +94,12 @@ WSL では `CODEX_HOME` がなく Linux 側 `~/.codex/config.toml` もないと ちょうど一つならそのディレクトリを使い、WSL app-server モードと Windows Codex Desktop が同じ config と auth ファイルを共有します。この検出を上書きするには `CODEX_HOME` を明示してください。 +Windows の Orca シェルは `CODEX_HOME` と `ORCA_CODEX_HOME` を Orca のバンドル runtime home に +設定することがありますが、ChatGPT/Codex App は引き続き `%USERPROFILE%\\.codex` を読みます。 +`ocx status` と `ocx doctor` はこの正確な不一致を検出し、ユーザーパスをマスクして対象 home を表示します。 +その Orca シェルからバックグラウンドサービスをインストールした場合は、まず元のシェルでサービスを削除し、 +App home を `CODEX_HOME` に設定して `ORCA_CODEX_HOME` を解除した後、同期/復元とサービスインストールをやり直してください。 + 専用プロバイダーモードの `requires_openai_auth = true` は Codex App/TUI のアカウントゲート UI がネイティブ Codex と同じ条件で動作するようにします。opencodex は `/v1/responses` WebSocket も提供します。専用 プロバイダーは `"websockets": true` のときのみ `supports_websockets = true` を宣言します。ループバックでは diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md index 3b803db6c..96463b65b 100644 --- a/docs-site/src/content/docs/ja/reference/cli.md +++ b/docs-site/src/content/docs/ja/reference/cli.md @@ -62,7 +62,8 @@ ocx eject back ### `ocx status [--json]` プロキシ PID、`/healthz` 接続状態、ダッシュボード URL、設定ファイルパス、デフォルトプロバイダー、Codex 自動起動 -設定、サービス状態、shim 状態を読み取り専用の診断要約として出力します。 +設定、サービス状態、shim 状態、ユーザー名をマスクした実効 Codex home を読み取り専用の診断要約として出力します。 +明示的で信頼度の高い Windows Orca runtime-home パターンに一致した場合のみ、App home との不一致に対する実行可能な警告を追加し、`CODEX_HOME` は自動変更しません。 機械が読める読み取り専用の診断契約は `--json` で受け取ります。 @@ -95,6 +96,13 @@ ocx status --json "runtime": { "source": "bundled" }, + "codexHome": { + "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", + "appCodexHome": "C:\\Users\\[USER]\\.codex", + "mismatch": false, + "warning": null, + "action": null + }, "codexAutostart": true, "defaultProvider": "openai", "service": { @@ -387,7 +395,9 @@ ocx codex-shim uninstall ### `ocx doctor` 状態パスとファイルシステムの種類、WSL 二重インストール、プロキシ環境/設定、ChatGPT 接続状態、Codex プラグインと -プロジェクト設定の警告、保留中の履歴マイグレーションを読み取り専用で診断します。復旧案内は出力しますが自ら適用はしません。 +プロジェクト設定の警告、保留中の履歴マイグレーションを読み取り専用で診断します。Codex app-home targeting +セクションは Windows Orca runtime-home の不一致を限定的に検出し、必要なら元の Orca サービス削除と App +home での再インストールを案内します。新しい診断のパスは OS ユーザー名をマスクします。復旧案内は出力しますが自ら適用はしません。 ### `ocx debug [provider|usage …]` diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index cda0e0d9c..2e9e68adb 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -93,6 +93,12 @@ WSL에서는 `CODEX_HOME`이 없고 Linux 쪽 `~/.codex/config.toml`도 없을 정확히 하나면 그 디렉터리를 사용하므로 WSL app-server mode와 Windows Codex Desktop이 같은 config와 auth 파일을 공유합니다. 이 탐지를 덮어쓰려면 `CODEX_HOME`을 명시하세요. +Windows의 Orca 셸은 `CODEX_HOME`과 `ORCA_CODEX_HOME`을 Orca 번들 런타임 home으로 설정할 수 있지만, +ChatGPT/Codex 앱은 계속 `%USERPROFILE%\\.codex`를 읽습니다. `ocx status`와 `ocx doctor`는 이 정확한 +불일치를 탐지해 사용자 경로를 가린 상태로 대상 home을 표시합니다. 해당 Orca 셸에서 백그라운드 서비스를 +설치했다면 먼저 원래 셸에서 서비스를 제거하고, 앱 home으로 `CODEX_HOME`을 설정하고 +`ORCA_CODEX_HOME`을 해제한 뒤 동기화/복원 및 서비스 설치를 다시 실행하세요. + 전용 프로바이더 모드의 `requires_openai_auth = true`는 Codex App/TUI의 계정 게이트 UI가 네이티브 Codex와 같은 조건으로 동작하게 합니다. opencodex는 `/v1/responses` WebSocket도 제공합니다. 전용 프로바이더는 `"websockets": true`일 때만 `supports_websockets = true`를 광고합니다. loopback에서는 diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md index 25231c742..db08f984f 100644 --- a/docs-site/src/content/docs/ko/reference/cli.md +++ b/docs-site/src/content/docs/ko/reference/cli.md @@ -65,7 +65,8 @@ ocx eject back ### `ocx status [--json]` 프록시 PID, `/healthz` 연결 상태, 대시보드 URL, 설정 파일 경로, 기본 프로바이더, Codex 자동 시작 -설정, 서비스 상태, shim 상태를 읽기 전용 진단 요약으로 출력합니다. +설정, 서비스 상태, shim 상태와 사용자 이름을 가린 실제 Codex home을 읽기 전용 진단 요약으로 출력합니다. +명시적인 고신뢰 Windows Orca runtime-home 패턴이 확인될 때만 App home 불일치에 대한 조치 가능한 경고를 추가하며 `CODEX_HOME`을 자동으로 바꾸지는 않습니다. 기계가 읽을 수 있는 읽기 전용 진단 계약은 `--json`으로 받습니다. @@ -98,6 +99,13 @@ ocx status --json "runtime": { "source": "bundled" }, + "codexHome": { + "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", + "appCodexHome": "C:\\Users\\[USER]\\.codex", + "mismatch": false, + "warning": null, + "action": null + }, "codexAutostart": true, "defaultProvider": "openai", "service": { @@ -405,7 +413,9 @@ ocx codex-shim uninstall ### `ocx doctor` 상태 경로와 파일시스템 유형, WSL 이중 설치, 프록시 환경/설정, ChatGPT 연결 상태, Codex 플러그인과 -프로젝트 설정 경고, 대기 중인 기록 마이그레이션을 읽기 전용으로 진단합니다. 복구 안내는 출력하지만 +프로젝트 설정 경고, 대기 중인 기록 마이그레이션을 읽기 전용으로 진단합니다. Codex app-home targeting +섹션은 Windows Orca runtime-home 불일치를 좁게 탐지하고 필요한 경우 기존 Orca 서비스 제거와 앱 home +기반 재설치 절차를 안내합니다. 새 진단의 경로에서는 OS 사용자 이름을 가립니다. 복구 안내는 출력하지만 직접 적용하지 않습니다. ### `ocx debug [provider|usage …]` diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 110a5989d..c0cd80164 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -65,7 +65,9 @@ Idempotently ensure a background proxy is running, then sync its live model cata ### `ocx status [--json]` Print a read-only diagnostic summary: proxy PID, `/healthz` reachability, dashboard URL, -config path, default provider, Codex autostart setting, service state, and shim state. +config path, default provider, Codex autostart setting, service state, shim state, and the redacted +effective Codex home. Only the explicit, high-confidence Windows Orca runtime-home signature adds an +actionable App-home mismatch warning; it never changes `CODEX_HOME` automatically. Human output also includes an **OAuth health** block after the OAuth logins summary: `OAuth health: ok` when every known account is healthy, or `OAuth health: warning` with one redacted line per @@ -105,6 +107,13 @@ Abbreviated example shape: "runtime": { "source": "bundled" }, + "codexHome": { + "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", + "appCodexHome": "C:\\Users\\[USER]\\.codex", + "mismatch": false, + "warning": null, + "action": null + }, "codexAutostart": true, "defaultProvider": "openai", "service": { @@ -415,7 +424,9 @@ lightweight, on-demand startup without a daemon — the proxy starts only when ` Run read-only environment and connectivity diagnostics: state paths and filesystem type, WSL dual installs, proxy environment/config, ChatGPT reachability, Codex plugin and project-config warnings, -and pending history migration. It prints repair hints but does not apply them. +and pending history migration. The Codex app-home targeting section also detects the narrow Windows +Orca runtime-home mismatch and explains service migration when applicable. Paths shown by this new +diagnostic redact the OS username. Doctor prints repair hints but does not apply them. The **OAuth reliability** section reports whether credential storage is writable, whether refresh single-flight / lock files can be created under `OPENCODEX_HOME`, non-healthy OAuth or Codex pool diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index afc655ec8..8b8ba02f3 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -103,6 +103,13 @@ $CODEX_HOME/models_cache.json каталог, чтобы режим app-server в WSL и Windows Codex Desktop разделяли одни и те же файлы конфигурации и аутентификации. Чтобы переопределить это обнаружение, задайте `CODEX_HOME` явно. +В Windows оболочка Orca может назначить `CODEX_HOME` и `ORCA_CODEX_HOME` встроенному runtime-home +Orca, тогда как ChatGPT/Codex App по-прежнему читает `%USERPROFILE%\\.codex`. `ocx status` и +`ocx doctor` обнаруживают именно это расхождение и показывают целевой home со скрытым именем +пользователя. Если фоновая служба была установлена из оболочки Orca, сначала удалите её в исходной +оболочке, затем задайте `CODEX_HOME` для App, удалите `ORCA_CODEX_HOME`, повторите sync/restore и +установите службу заново. + В режиме выделенного провайдера `requires_openai_auth = true` сохраняет поведение зависящих от аккаунта интерфейсов Codex App/TUI таким же, как в нативном Codex. opencodex также обслуживает `/v1/responses` по WebSocket. Выделенный провайдер объявляет `supports_websockets = true` только diff --git a/docs-site/src/content/docs/ru/reference/cli.md b/docs-site/src/content/docs/ru/reference/cli.md index f3fb33ca0..a64ded9b4 100644 --- a/docs-site/src/content/docs/ru/reference/cli.md +++ b/docs-site/src/content/docs/ru/reference/cli.md @@ -71,7 +71,9 @@ ocx eject back Печатает диагностическую сводку только для чтения: PID прокси, доступность `/healthz`, URL дашборда, путь к конфигурации, провайдера по умолчанию, настройку автозапуска Codex, состояние -сервиса и состояние shim. +сервиса и состояние shim, а также эффективный Codex home со скрытым именем пользователя. Только при +явном высоконадёжном совпадении с сигнатурой Windows Orca runtime-home добавляется инструкция по +исправлению расхождения с App home; `CODEX_HOME` автоматически не меняется. Используйте `--json` для машиночитаемого диагностического контракта только для чтения: @@ -104,6 +106,13 @@ ocx status --json "runtime": { "source": "bundled" }, + "codexHome": { + "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", + "appCodexHome": "C:\\Users\\[USER]\\.codex", + "mismatch": false, + "warning": null, + "action": null + }, "codexAutostart": true, "defaultProvider": "openai", "service": { @@ -429,8 +438,10 @@ ocx codex-shim uninstall Выполняет диагностику окружения и связности только для чтения: пути состояния и тип файловой системы, двойные установки в WSL, окружение/конфигурация прокси, доступность ChatGPT, -предупреждения о плагине Codex и конфигурации проекта, а также ожидающая миграция истории. -Команда печатает подсказки по исправлению, но не применяет их. +предупреждения о плагине Codex и конфигурации проекта, а также ожидающая миграция истории. Раздел +Codex app-home targeting также узко обнаруживает расхождение Windows Orca runtime-home и при +необходимости объясняет миграцию службы. В новых путях скрывается имя пользователя ОС. Команда +печатает подсказки по исправлению, но не применяет их. ### `ocx debug [provider|usage …]` diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index e677fc0eb..5fd0fd57d 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -91,6 +91,12 @@ $CODEX_HOME/models_cache.json 使用该目录,让 WSL app-server mode 和 Windows Codex Desktop 共享同一份 config 与 auth 文件。要覆盖 此检测,请显式设置 `CODEX_HOME`。 +在 Windows 上,Orca shell 可能同时把 `CODEX_HOME` 和 `ORCA_CODEX_HOME` 指向 Orca 的内置 +runtime home,而 ChatGPT/Codex App 仍读取 `%USERPROFILE%\\.codex`。`ocx status` 与 +`ocx doctor` 会检测这一明确的不一致,并以隐藏用户名的形式显示目标 home。如果后台服务是在原 Orca +shell 中安装的,请先在原 shell 中卸载服务,再把 `CODEX_HOME` 设为 App home、取消 +`ORCA_CODEX_HOME`,重新同步/恢复并安装服务。 + 在专用提供商模式下,`requires_openai_auth = true` 会让 Codex App/TUI 的账号门控界面与原生 Codex 保持一致。opencodex 也提供 `/v1/responses` WebSocket。专用提供商仅在 `"websockets": true` 时声明 `supports_websockets = true`;loopback 模式下,Codex 的内置提供商 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli.md b/docs-site/src/content/docs/zh-cn/reference/cli.md index dc76b6cab..b4af91302 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli.md @@ -63,7 +63,8 @@ ocx eject back ### `ocx status [--json]` 打印只读诊断摘要:代理 PID、`/healthz` 可达性、仪表盘 URL、配置路径、默认 provider、Codex -自动启动设置、服务状态和 shim 状态。 +自动启动设置、服务状态、shim 状态以及隐藏用户名后的实际 Codex home。仅当命中明确的高置信度 +Windows Orca runtime-home 特征时,才会针对 App home 不一致给出可执行警告,但不会自动修改 `CODEX_HOME`。 使用 `--json` 可获得机器可读的只读诊断契约: @@ -96,6 +97,13 @@ ocx status --json "runtime": { "source": "bundled" }, + "codexHome": { + "effectiveCodexHome": "C:\\Users\\[USER]\\.codex", + "appCodexHome": "C:\\Users\\[USER]\\.codex", + "mismatch": false, + "warning": null, + "action": null + }, "codexAutostart": true, "defaultProvider": "openai", "service": { @@ -389,7 +397,9 @@ ocx codex-shim uninstall ### `ocx doctor` 运行只读的环境与连接诊断:状态路径和文件系统类型、WSL 双重安装、代理环境/配置、ChatGPT -可达性、Codex plugin 与项目配置警告,以及待处理的历史迁移。它会打印修复建议,但不会执行。 +可达性、Codex plugin 与项目配置警告,以及待处理的历史迁移。Codex app-home targeting 部分还会 +窄范围检测 Windows Orca runtime-home 不一致,并在需要时说明如何迁移服务。新诊断中的路径会隐藏 +OS 用户名。它会打印修复建议,但不会执行。 ### `ocx debug [provider|usage …]` diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 19d84d054..abbb81682 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -15,7 +15,7 @@ import { gracefulStopHost } from "../lib/process-control"; import { maskAccountId } from "../lib/privacy"; import { loadServiceTokenFromFile } from "../lib/service-secrets"; import { readCodexTokens } from "../codex/auth-collision"; -import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home"; +import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home"; import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim"; import { countPendingOpencodexHistory } from "../codex/history-provider"; import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings"; @@ -636,6 +636,16 @@ export async function runDoctor(args: string[] = []): Promise { console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`); } + const orcaHome = collectOrcaCodexHomeDiagnostic(); + console.log("\nCodex app home targeting"); + console.log(` ${orcaHome.mismatch ? "!! " : "ok "} Effective Codex home: ${orcaHome.effectiveCodexHome}`); + if (orcaHome.mismatch) { + console.log(` !! ${orcaHome.warning}`); + console.log(` Action: ${orcaHome.action}`); + } else { + console.log(" No Orca-owned CODEX_HOME mismatch detected."); + } + const doctorConfig = readConfigDiagnostics().config; const startup = collectStartupHealth(doctorConfig); console.log("\nCodex restart safety"); diff --git a/src/cli/index.ts b/src/cli/index.ts index f8f4c0d67..bb2dab8c6 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -42,6 +42,7 @@ import { maybeShowStarPrompt } from "./star-prompt"; import { maybeShowUpdatePrompt } from "../update/notify"; import { syncModelsToCodex } from "../codex/sync"; import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job"; +import { collectOrcaCodexHomeDiagnostic } from "../codex/home"; const args = process.argv.slice(2); const command = args[0]; @@ -615,6 +616,11 @@ async function handleStatus() { console.log(` Codex runtime: ${status.json.codexRuntime.path}`); console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`); console.log(` Codex source: ${status.json.codexRuntime.source}`); + console.log(` Codex home: ${status.json.codexHome.effectiveCodexHome}`); + if (status.json.codexHome.warning) { + console.log(` ⚠️ ${status.json.codexHome.warning}`); + console.log(` Action: ${status.json.codexHome.action}`); + } console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`); if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) { console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`); @@ -688,7 +694,8 @@ switch (command) { process.exit(1); } await syncModelsToCodex(live.port); - console.log("Plain `codex` now routes through opencodex again (undo with: ocx restore)."); + const target = collectOrcaCodexHomeDiagnostic(); + console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`); break; } const r = restoreNativeCodex(); diff --git a/src/cli/status.ts b/src/cli/status.ts index 84d2a3a31..48bd8adc2 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -9,6 +9,7 @@ import { getCodexRoutingKind } from "../codex/inject"; import { diagnoseCodexShim } from "../codex/shim"; import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime"; import { redactSecretString, redactUserPath } from "../lib/redact"; +import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home"; type HealthCheck = { ok: boolean; @@ -65,6 +66,7 @@ export type CliStatusJson = { runtimeVersion: string | null; }; }; + codexHome: OrcaCodexHomeDiagnostic; }; export type CliStatusView = { @@ -163,6 +165,7 @@ export async function collectStatus(): Promise { })(); const lastClamp = loadLastEffortClamp(); const clampActive = effortClampAppliesToRuntime(lastClamp, resolvedRuntime.runtime); + const codexHome = collectOrcaCodexHomeDiagnostic(); const warningParts: string[] = []; if ( resolvedRuntime.replacedConfigured @@ -256,6 +259,7 @@ export async function collectStatus(): Promise { codexShim: { summary: codexShimSummary }, codexPlugins, codexRuntime, + codexHome, }, }; } diff --git a/src/codex/home.ts b/src/codex/home.ts index e1d24f454..38e4af914 100644 --- a/src/codex/home.ts +++ b/src/codex/home.ts @@ -1,7 +1,8 @@ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; import { homedir } from "node:os"; -import { join, posix, resolve } from "node:path"; +import { join, posix, resolve, win32 } from "node:path"; import { expandUserPath } from "../config"; +import { redactUserPath } from "../lib/redact"; export type CodexHomeDeps = { env?: NodeJS.ProcessEnv; @@ -144,3 +145,62 @@ export function resolveCodexHomeDir(deps: CodexHomeDeps = {}): string { if (raw) return resolve(expandUserPath(raw)); return defaultCodexHome(deps); } + +export type OrcaCodexHomeDiagnostic = { + applicable: boolean; + mismatch: boolean; + effectiveCodexHome: string; + appCodexHome: string; + orcaCodexHome: string | null; + warning: string | null; + action: string | null; +}; + +type OrcaCodexHomeDeps = CodexHomeDeps & { + effectiveCodexHome?: string; + appCodexHome?: string; +}; + +function normalizedWindowsPath(path: string): string { + return path.trim().replaceAll("/", "\\").replace(/\\+$/, "").toLowerCase(); +} + +/** + * High-confidence Orca/ChatGPT dual-home diagnosis. Explicit CODEX_HOME remains + * authoritative; this only explains when an Orca-owned shell targets a home the + * Windows ChatGPT/Codex app does not read. + */ +export function collectOrcaCodexHomeDiagnostic(deps: OrcaCodexHomeDeps = {}): OrcaCodexHomeDiagnostic { + const platform = deps.platform ?? process.platform; + const env = deps.env ?? process.env; + const effectiveCodexHome = deps.effectiveCodexHome ?? resolveCodexHomeDir(deps); + const appCodexHome = deps.appCodexHome + ?? (platform === "win32" ? win32.join((deps.homedir ?? homedir)(), ".codex") : join((deps.homedir ?? homedir)(), ".codex")); + const explicitHome = env.CODEX_HOME?.trim() ?? ""; + const orcaCodexHome = env.ORCA_CODEX_HOME?.trim() || null; + const normalizedEffective = normalizedWindowsPath(effectiveCodexHome); + const normalizedOrca = orcaCodexHome ? normalizedWindowsPath(orcaCodexHome) : ""; + const normalizedApp = normalizedWindowsPath(appCodexHome); + const applicable = platform === "win32" + && !!explicitHome + && !!orcaCodexHome + && normalizedEffective === normalizedOrca + && /(?:^|\\)orca\\codex-runtime-home\\home$/i.test(normalizedOrca); + const mismatch = applicable && normalizedEffective !== normalizedApp; + const displayEffective = redactUserPath(effectiveCodexHome); + const displayApp = redactUserPath(appCodexHome); + const displayOrca = orcaCodexHome ? redactUserPath(orcaCodexHome) : null; + return { + applicable, + mismatch, + effectiveCodexHome: displayEffective, + appCodexHome: displayApp, + orcaCodexHome: displayOrca, + warning: mismatch + ? `CODEX_HOME targets Orca's runtime home (${displayEffective}), while the Windows ChatGPT/Codex app uses ${displayApp}; OpenCodex injection will not reach that app.` + : null, + action: mismatch + ? "If a service was installed from Orca, run 'ocx service uninstall' in that original Orca shell first. Then in Command Prompt run set \"ORCA_CODEX_HOME=\" and set \"CODEX_HOME=%USERPROFILE%\\.codex\"; or in PowerShell run Remove-Item Env:ORCA_CODEX_HOME -ErrorAction SilentlyContinue and $env:CODEX_HOME = Join-Path $env:USERPROFILE '.codex'. Rerun the command, then reinstall with 'ocx service install'." + : null, + }; +} diff --git a/src/codex/sync.ts b/src/codex/sync.ts index f704bfc39..63a44189b 100644 --- a/src/codex/sync.ts +++ b/src/codex/sync.ts @@ -3,6 +3,7 @@ import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, import { refreshCodexModelCatalog } from "./refresh"; import { applyProxyEnv, loadConfig } from "../config"; import type { OcxConfig } from "../types"; +import { collectOrcaCodexHomeDiagnostic } from "./home"; export interface CodexSyncResult { ok: boolean; @@ -20,6 +21,7 @@ interface CodexSyncDeps { refreshCodexModelCatalog: typeof refreshCodexModelCatalog; injectCodexConfig: typeof injectCodexConfig; currentExternalCodexModelProvider?: typeof currentExternalCodexModelProvider; + collectCodexHomeDiagnostic?: typeof collectOrcaCodexHomeDiagnostic; } const defaultDeps: CodexSyncDeps = { @@ -27,6 +29,19 @@ const defaultDeps: CodexSyncDeps = { injectCodexConfig, }; +function reportCodexHomeTarget( + log: Pick | null, + collectDiagnostic: typeof collectOrcaCodexHomeDiagnostic, +): void { + if (!log) return; + const target = collectDiagnostic(); + log.log(` Target Codex home: ${target.effectiveCodexHome}`); + if (target.warning) { + log.error(`WARNING: ${target.warning}`); + log.error(`Action: ${target.action}`); + } +} + export async function syncModelsToCodex( port?: number, config: OcxConfig = loadConfig(), @@ -38,6 +53,7 @@ export async function syncModelsToCodex( if (externalProvider) { const result = await deps.injectCodexConfig(p, config, {}); log?.log(result.message); + reportCodexHomeTarget(log, deps.collectCodexHomeDiagnostic ?? collectOrcaCodexHomeDiagnostic); return { ok: result.success, added: 0, @@ -76,6 +92,7 @@ export async function syncModelsToCodex( const result = await deps.injectCodexConfig(p, config, { catalogPath: catalogPathForInjection }); log?.log(result.message); + reportCodexHomeTarget(log, deps.collectCodexHomeDiagnostic ?? collectOrcaCodexHomeDiagnostic); const projectConfigWarnings = printProjectCodexConfigWarnings(log, { cwd: process.cwd() }); return { ok: result.success, diff --git a/structure/02_config-and-codex-home.md b/structure/02_config-and-codex-home.md index 9dd4fdaab..37ff41f2e 100644 --- a/structure/02_config-and-codex-home.md +++ b/structure/02_config-and-codex-home.md @@ -15,6 +15,24 @@ $CODEX_HOME/models_cache.json Never assume macOS-only paths. Windows, service installs, and app-launched Codex can all depend on the resolved `CODEX_HOME`. +OpenCodex never overrides an explicit `CODEX_HOME`. On Windows, `ocx doctor` and `ocx status` +nevertheless diagnose the high-confidence Orca dual-home case: both `CODEX_HOME` and +`ORCA_CODEX_HOME` select Orca's `orca/codex-runtime-home/home`, while the ChatGPT/Codex app uses the +default `%USERPROFILE%\\.codex`. Sync and restore output always prints the exact target Codex home; +display and JSON paths redact the OS username. The diagnostic tells users to invoke OpenCodex with +the app home explicitly rather than silently claiming that an unrelated app was configured. If a +service was installed under the Orca home, it must first be uninstalled from that original Orca +environment and then reinstalled under the app home; changing only the current shell cannot migrate +the recorded service ownership. + +[Decision Log] +- 목적과 의도: Make multi-home injection truthful without taking ownership of user environment variables. +- 기존 구현 및 제약 조건: CODEX_HOME is an intentional override, but Orca exports it for its own bundled runtime and the Windows app reads a different home. +- 검토한 주요 대안: Rewrite CODEX_HOME automatically, warn for every custom home, or detect only the Orca-owned signature and report the target path. +- 선택한 방식: Preserve the override, add a narrow Windows/Orca diagnostic, and qualify sync/restore success output with the effective home. +- 다른 대안 대신 이 방식을 선택한 이유: It fixes the silent failure while avoiding destructive or noisy behavior for intentional custom homes. +- 장점, 단점 및 영향: Orca users get an actionable warning; other multi-home products remain unchanged until they have an equally reliable signature. + `atomicWriteFile` uses a temp file named `{path}.ocx.{pid}.{seq}.tmp` (process ID + incrementing sequence number) to avoid collisions when concurrent writers (e.g. `ocx stop` and the proxy's own shutdown handler) both restore Codex config simultaneously. The temp is renamed atomically into place. diff --git a/tests/cli-restore-back.test.ts b/tests/cli-restore-back.test.ts index 05e032fac..209661179 100644 --- a/tests/cli-restore-back.test.ts +++ b/tests/cli-restore-back.test.ts @@ -15,6 +15,7 @@ describe("ocx restore back", () => { expect(restoreCase).toContain("await findLiveProxy()"); expect(restoreCase).toContain("await syncModelsToCodex(live.port)"); expect(restoreCase.indexOf("findLiveProxy()")).toBeLessThan(restoreCase.indexOf("syncModelsToCodex(live.port)")); + expect(restoreCase).toContain("target.effectiveCodexHome"); // The forward switch (plain `ocx restore`) is unchanged. expect(restoreCase).toContain("restoreNativeCodex()"); }); diff --git a/tests/cli-status-json.test.ts b/tests/cli-status-json.test.ts index e3eaa69f9..00da9bc1a 100644 --- a/tests/cli-status-json.test.ts +++ b/tests/cli-status-json.test.ts @@ -76,6 +76,13 @@ describe("CLI status JSON", () => { newerAvailable?: unknown; catalogClamp?: { active?: unknown; removedEfforts?: unknown; runtimeVersion?: unknown }; }; + codexHome?: { + effectiveCodexHome?: unknown; + appCodexHome?: unknown; + mismatch?: unknown; + warning?: unknown; + action?: unknown; + }; }; expect(parsed.schemaVersion).toBe(1); @@ -117,6 +124,10 @@ describe("CLI status JSON", () => { expect(parsed.codexRuntime?.catalogClamp?.active).toBe(false); expect(Array.isArray(parsed.codexRuntime?.catalogClamp?.removedEfforts)).toBe(true); expect(parsed.codexRuntime?.catalogClamp?.runtimeVersion).toBeNull(); + expect(typeof parsed.codexHome?.effectiveCodexHome).toBe("string"); + expect(typeof parsed.codexHome?.appCodexHome).toBe("string"); + expect(typeof parsed.codexHome?.mismatch).toBe("boolean"); + expect(parsed.codexHome?.warning === null || typeof parsed.codexHome?.warning === "string").toBe(true); const serialized = JSON.stringify(parsed).toLowerCase(); for (const forbidden of ["apikey", "sk-test-secret", "token", "refreshtoken", "authorization", "email"]) { diff --git a/tests/codex-sync-api.test.ts b/tests/codex-sync-api.test.ts index d7f6d4e3a..3ab741ec8 100644 --- a/tests/codex-sync-api.test.ts +++ b/tests/codex-sync-api.test.ts @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { syncModelsToCodex } from "../src/codex/sync"; import type { OcxConfig } from "../src/types"; +import type { OrcaCodexHomeDiagnostic } from "../src/codex/home"; const TEST_DIR = join(import.meta.dir, ".tmp-codex-sync-api"); const TEST_CODEX_HOME = join(TEST_DIR, "codex"); @@ -14,6 +15,19 @@ const config = { providers: {}, } as OcxConfig; +function homeDiagnostic(overrides: Partial = {}): OrcaCodexHomeDiagnostic { + return { + applicable: false, + mismatch: false, + effectiveCodexHome: "C:\\Users\\[USER]\\.codex", + appCodexHome: "C:\\Users\\[USER]\\.codex", + orcaCodexHome: null, + warning: null, + action: null, + ...overrides, + }; +} + describe("GUI/CLI Codex sync backend", () => { beforeEach(() => { prevCodexHome = process.env.CODEX_HOME; @@ -32,7 +46,9 @@ describe("GUI/CLI Codex sync backend", () => { let injectedPort = 0; let injectedCatalogPath: string | null | undefined; - const result = await syncModelsToCodex(12345, config, null, { + const logs: string[] = []; + const errors: string[] = []; + const result = await syncModelsToCodex(12345, config, { log: line => logs.push(String(line)), error: line => errors.push(String(line)) }, { refreshCodexModelCatalog: async () => ({ added: 3, path: "/tmp/opencodex-catalog.json", @@ -45,6 +61,7 @@ describe("GUI/CLI Codex sync backend", () => { return { success: true, message: "injected" }; }, currentExternalCodexModelProvider: () => null, + collectCodexHomeDiagnostic: () => homeDiagnostic(), }); expect(injectedPort).toBe(12345); @@ -57,6 +74,8 @@ describe("GUI/CLI Codex sync backend", () => { cacheSynced: true, message: "injected", }); + expect(logs).toContain(" Target Codex home: C:\\Users\\[USER]\\.codex"); + expect(errors).toEqual([]); }); test("keeps injection fallback behavior when catalog refresh throws", async () => { @@ -82,7 +101,17 @@ describe("GUI/CLI Codex sync backend", () => { test("skips catalog refresh before preserving an external provider", async () => { let refreshed = false; let injectedCatalogPath: string | null | undefined = "unset"; - const result = await syncModelsToCodex(10100, config, null, { + const logs: string[] = []; + const errors: string[] = []; + const mismatch = homeDiagnostic({ + applicable: true, + mismatch: true, + effectiveCodexHome: "C:\\Users\\[USER]\\AppData\\Roaming\\orca\\codex-runtime-home\\home", + orcaCodexHome: "C:\\Users\\[USER]\\AppData\\Roaming\\orca\\codex-runtime-home\\home", + warning: "Orca target does not reach the app", + action: "migrate the installed service", + }); + const result = await syncModelsToCodex(10100, config, { log: line => logs.push(String(line)), error: line => errors.push(String(line)) }, { refreshCodexModelCatalog: async () => { refreshed = true; throw new Error("must not refresh"); @@ -92,6 +121,7 @@ describe("GUI/CLI Codex sync backend", () => { return { success: true, message: "external provider preserved" }; }, currentExternalCodexModelProvider: () => "custom", + collectCodexHomeDiagnostic: () => mismatch, }); expect(refreshed).toBe(false); @@ -104,5 +134,10 @@ describe("GUI/CLI Codex sync backend", () => { cacheSynced: false, message: "external provider preserved", }); + expect(logs).toContain(` Target Codex home: ${mismatch.effectiveCodexHome}`); + expect(errors).toEqual([ + `WARNING: ${mismatch.warning}`, + `Action: ${mismatch.action}`, + ]); }); }); diff --git a/tests/doctor.test.ts b/tests/doctor.test.ts index 47e6e9831..b94c686c8 100644 --- a/tests/doctor.test.ts +++ b/tests/doctor.test.ts @@ -17,6 +17,7 @@ import { resolveCodexHomeDir, type ServiceMemoryData, } from "../src/cli/doctor"; +import { collectOrcaCodexHomeDiagnostic } from "../src/codex/home"; const TEST_DIR = join(import.meta.dir, ".tmp-doctor-test"); const TEST_CODEX_HOME = join(TEST_DIR, "codex"); @@ -77,6 +78,42 @@ describe("doctor", () => { expect(resolveCodexHomeDir()).toBe(join(homedir(), "custom-codex")); }); + test("Orca home diagnostic warns only for the Windows Orca runtime mismatch", () => { + const appHome = "C:\\Users\\alice\\.codex"; + const orcaHome = "C:\\Users\\alice\\AppData\\Roaming\\orca\\codex-runtime-home\\home"; + const mismatch = collectOrcaCodexHomeDiagnostic({ + platform: "win32", + env: { CODEX_HOME: orcaHome, ORCA_CODEX_HOME: orcaHome }, + effectiveCodexHome: orcaHome, + appCodexHome: appHome, + }); + expect(mismatch.mismatch).toBe(true); + expect(mismatch.warning).toContain("OpenCodex injection will not reach that app"); + expect(mismatch.effectiveCodexHome).toContain("C:\\Users\\[USER]\\"); + expect(mismatch.effectiveCodexHome).not.toContain("alice"); + expect(mismatch.action).toContain("ocx service uninstall"); + expect(mismatch.action).toContain("ocx service install"); + expect(mismatch.action).toContain("%USERPROFILE%\\.codex"); + expect(mismatch.action).toContain("Remove-Item Env:ORCA_CODEX_HOME"); + expect(mismatch.action).not.toContain("C:\\Users\\[USER]"); + + const matching = collectOrcaCodexHomeDiagnostic({ + platform: "win32", + env: { CODEX_HOME: appHome, ORCA_CODEX_HOME: orcaHome }, + effectiveCodexHome: appHome, + appCodexHome: appHome, + }); + expect(matching.mismatch).toBe(false); + + const intentionalCustom = collectOrcaCodexHomeDiagnostic({ + platform: "win32", + env: { CODEX_HOME: "D:\\codex-work" }, + effectiveCodexHome: "D:\\codex-work", + appCodexHome: appHome, + }); + expect(intentionalCustom.mismatch).toBe(false); + }); + test("resolveCodexHomeDir discovers a single Windows Codex Desktop home from WSL", () => { delete process.env.CODEX_HOME; const wslHome = join(TEST_DIR, "wsl-home");