From 7699b9d1af54bceafc34e399c93d330bab43205e Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 11:36:25 +0900 Subject: [PATCH 01/11] fix: surface restart-unsafe Codex routing --- .../src/content/docs/guides/web-dashboard.md | 4 +- .../content/docs/ja/guides/web-dashboard.md | 4 +- .../content/docs/ko/guides/web-dashboard.md | 4 +- .../content/docs/ru/guides/web-dashboard.md | 4 +- .../docs/zh-cn/guides/web-dashboard.md | 4 +- gui/src/App.tsx | 27 ++- gui/src/i18n/de.ts | 47 +++- gui/src/i18n/en.ts | 49 +++- gui/src/i18n/ja.ts | 49 +++- gui/src/i18n/ko.ts | 49 +++- gui/src/i18n/ru.ts | 49 +++- gui/src/i18n/zh.ts | 49 +++- gui/src/pages/Dashboard.tsx | 25 ++- gui/src/pages/Startup.tsx | 211 ++++++++++++++++++ gui/src/styles.css | 38 ++++ src/cli/doctor.ts | 9 + src/cli/index.ts | 2 + src/cli/status.ts | 18 +- src/codex/autostart-health.ts | 107 +++++++++ src/codex/inject.ts | 24 +- src/codex/shim.ts | 32 ++- src/server/management/config-routes.ts | 12 +- structure/05_gui-and-management-api.md | 9 + tests/autostart-health.test.ts | 91 ++++++++ tests/cli-status-json.test.ts | 20 ++ tests/codex-shim.test.ts | 4 +- 26 files changed, 916 insertions(+), 25 deletions(-) create mode 100644 gui/src/pages/Startup.tsx create mode 100644 src/codex/autostart-health.ts create mode 100644 tests/autostart-health.test.ts diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 378fc287916..d5d0f1c5347 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -29,7 +29,8 @@ bun run dev:gui | **Sub-agent delegation** | Choose a native or routed guidance model and an optional reasoning effort for v1 delegation prompts. This is not a per-spawn router; see below. | | **Sidecars** | Choose the web-search model and effort plus the vision-description model. Changes apply on the next request. | | **Maintenance** | Resync the Codex model catalog, inspect project-local config bypass warnings, check the latest or preview release, and run an update with optional proxy restart. | -| **Codex autostart** | Enable or disable the Codex launcher shim that runs `ocx ensure` before Codex CLI/App starts. | +| **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. | +| **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. | | **Providers** | Add, edit, enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. | | **Add provider** | Search registry-backed presets for account login, API-key services, local servers, or a custom endpoint. | | **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, and configure quota auto-switch and transient-failure failover. | @@ -86,6 +87,7 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints | Endpoint | Purpose | | --- | --- | | `GET` / `PUT /api/settings` | Read settings or toggle Codex autostart. | +| `GET /api/startup-health` | Read secret-free routing, service, shim, and restart-safety diagnostics. | | `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. | | `GET` / `PUT /api/sidecar-settings` | Read or set search/vision sidecar model settings. | diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md index d4a82d9a592..2505e258ad2 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -29,7 +29,8 @@ bun run dev:gui | **サブエージェント委任** | v1 委任プロンプトに入れるネイティブまたはルーティングモデルとオプションの推論強度を選びます。スポーンごとのルーターではありません。下記の説明を確認してください。 | | **サイドカー** | ウェブ検索モデルと強度、画像説明モデルを選択します。次回リクエストから適用されます。 | | **メンテナンス** | Codex モデルカタログを再同期し、プロジェクトローカル設定のバイパス警告を確認し、latest/preview 更新を照会またはオプションのプロキシ再起動と共にインストールします。 | -| **Codex 自動起動** | Codex CLI/App 実行前に `ocx ensure` を呼ぶランチャーシムをオン/オフします。 | +| **起動安全性** | 注入された Codex ルーティングが再起動後も機能するか、サービスと launcher shim の状態、正確な修復コマンドと共に表示します。 | +| **Codex 自動起動** | インストール済み Codex launcher shim に `ocx ensure` の実行を許可します。このトグルは shim やバックグラウンドサービスをインストールしません。 | | **プロバイダー** | プロバイダーを追加、編集、有効化/無効化、削除し、対応する OAuth アカウントプールと API キープールを管理します。 | | **プロバイダー追加** | レジストリベースのプリセットからアカウントログイン、API キーサービス、ローカルサーバー、custom エンドポイントを検索します。 | | **Codex 認証** | ChatGPT/Codex プールアカウントを追加し、次回セッションアカウントを選び、5 時間 / 週間 / 30 日クォータを更新し、クォータ自動切り替えと一時的失敗フェイルオーバーを設定します。 | @@ -81,6 +82,7 @@ GUI はプロキシの JSON 管理 API を使うシンクライアントです | エンドポイント | 用途 | --- | --- | | `GET` / `PUT /api/settings` | 設定を読むか Codex 自動起動をオン/オフします。 | +| `GET /api/startup-health` | 秘密情報を含まないルーティング、サービス、shim、再起動安全性診断を読み取ります。 | | `POST /api/sync` | 共有モデルカタログを再構築し Codex モデルキャッシュを古い状態としてマークします。 | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 自己更新作業を確認、実行、追跡します。 | | `GET` / `PUT /api/sidecar-settings` | 検索/ビジョンサイドカーモデル設定を読むか変えます。 | diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index 69952fe9e77..0a8d68f07b5 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -29,7 +29,8 @@ bun run dev:gui | **Sub-agent delegation** | v1 위임 프롬프트에 넣을 네이티브 또는 라우팅 모델과 선택적 reasoning 강도를 고릅니다. 스폰별 라우터는 아닙니다. 아래 설명을 확인하세요. | | **사이드카** | 웹 검색 모델과 강도, 이미지 설명 모델을 선택합니다. 다음 요청부터 적용됩니다. | | **Maintenance** | Codex 모델 카탈로그를 다시 동기화하고, 프로젝트 로컬 설정의 우회 경고를 확인하고, latest/preview 업데이트를 조회하거나 선택적 프록시 재시작과 함께 설치합니다. | -| **Codex 자동 시작** | Codex CLI/App 실행 전에 `ocx ensure`를 호출하는 launcher shim을 켜거나 끕니다. | +| **시작 안전성** | 주입된 Codex 라우팅이 재부팅 후에도 유지되는지 서비스와 launcher shim 상태, 정확한 복구 명령과 함께 표시합니다. | +| **Codex 자동 시작** | 이미 설치된 Codex launcher shim이 `ocx ensure`를 실행하도록 허용합니다. 이 토글은 shim이나 백그라운드 서비스를 설치하지 않습니다. | | **Providers** | 프로바이더를 추가, 편집, 활성화/비활성화, 제거하고, 지원되는 OAuth 계정 풀과 API key 풀을 관리합니다. | | **Add provider** | 레지스트리 기반 프리셋에서 계정 로그인, API key 서비스, 로컬 서버, custom endpoint를 검색합니다. | | **Codex Auth** | ChatGPT/Codex 풀 계정을 추가하고, 다음 세션 계정을 선택하고, 5시간 / 주간 / 30일 할당량을 갱신하며, 할당량 자동 전환과 일시적 실패 failover를 설정합니다. | @@ -83,6 +84,7 @@ GUI는 프록시의 JSON 관리 API를 사용하는 얇은 클라이언트입니 | 엔드포인트 | 용도 | | --- | --- | | `GET` / `PUT /api/settings` | 설정을 읽거나 Codex 자동 시작을 켜고 끕니다. | +| `GET /api/startup-health` | 비밀값 없이 라우팅, 서비스, shim, 재부팅 안전성 진단을 읽습니다. | | `POST /api/sync` | 공유 모델 카탈로그를 다시 만들고 Codex 모델 캐시를 오래된 상태로 표시합니다. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 자체 업데이트 작업을 확인, 실행, 추적합니다. | | `GET` / `PUT /api/sidecar-settings` | 검색/비전 사이드카 모델 설정을 읽거나 바꿉니다. | diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index 63bd25640a9..def6845a17e 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -29,7 +29,8 @@ bun run dev:gui | **Sub-agent delegation** | Выбор нативной или маршрутизируемой модели для инструкции и необязательного уровня рассуждений для v1-промптов делегирования. Это не маршрутизатор для каждого порождения; см. ниже. | | **Сайдкары** | Выбор модели и уровня рассуждений для веб-поиска, а также модели описания изображений. Изменения применяются со следующего запроса. | | **Maintenance** | Пересинхронизация каталога моделей Codex, просмотр предупреждений об обходе через проектную локальную конфигурацию, проверка последнего или предварительного выпуска и запуск обновления с необязательным перезапуском прокси. | -| **Автозапуск Codex** | Включение и отключение launcher-шима Codex, который выполняет `ocx ensure` перед запуском Codex CLI/App. | +| **Безопасность запуска** | Показывает, сохранит ли внедрённая маршрутизация Codex работоспособность после перезагрузки, отдельно отображая службу, launcher shim и точные команды исправления. | +| **Автозапуск Codex** | Разрешает уже установленному launcher shim Codex выполнять `ocx ensure`. Переключатель не устанавливает shim или фоновую службу. | | **Providers** | Добавление, редактирование, включение/отключение и удаление провайдеров; управление пулами OAuth-аккаунтов и пулами API-ключей там, где они поддерживаются. | | **Add provider** | Поиск по пресетам из реестра: вход по аккаунту, сервисы с API-ключом, локальные серверы или пользовательская конечная точка. | | **Codex Auth** | Добавление аккаунтов пула ChatGPT/Codex, выбор аккаунта для следующей сессии, обновление квот 5 ч / недельных / 30-дневных, настройка автопереключения по квоте и failover при временных сбоях. | @@ -86,6 +87,7 @@ GUI — это тонкий клиент поверх JSON-API управлен | Эндпоинт | Назначение | | --- | --- | | `GET` / `PUT /api/settings` | Чтение настроек или переключение автозапуска Codex. | +| `GET /api/startup-health` | Чтение безопасной диагностики маршрутизации, службы, shim и устойчивости к перезагрузке. | | `POST /api/sync` | Пересборка общего каталога моделей и инвалидация кэша моделей Codex. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Проверка, запуск и мониторинг задач самообновления. | | `GET` / `PUT /api/sidecar-settings` | Чтение или настройка моделей сайдкаров поиска/vision. | diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 9e651ff2d73..08fd0b6df68 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -28,7 +28,8 @@ bun run dev:gui | **Sub-agent delegation** | 为 v1 委派 prompt 选择原生或路由模型,并可指定 reasoning 强度。它不是逐次生成的路由器,详见下文。 | | **Sidecar** | 选择 web-search 模型及强度,以及图像描述模型;更改从下一次请求开始生效。 | | **Maintenance** | 重新同步 Codex 模型目录,查看项目级配置绕过警告,检查 latest/preview 版本,并可在更新后重启代理。 | -| **Codex 自动启动** | 启用或禁用 Codex launcher shim;该 shim 会在 Codex CLI/App 启动前运行 `ocx ensure`。 | +| **启动安全** | 显示注入的 Codex 路由能否在重启后继续工作,并分别显示服务、launcher shim 状态和准确的修复命令。 | +| **Codex 自动启动** | 允许已安装的 Codex launcher shim 运行 `ocx ensure`。此开关不会安装 shim 或后台服务。 | | **Providers** | 添加、编辑、启用/禁用、删除 provider,并在支持时管理 OAuth 账号池和 API key 池。 | | **Add provider** | 搜索 registry preset,选择账号登录、API key 服务、本地服务器或自定义 endpoint。 | | **Codex Auth** | 添加 ChatGPT/Codex 池账号,选择下一 session 的账号,刷新 5h / 每周 / 30d 配额,并设置配额自动切换和临时故障 failover。 | @@ -78,6 +79,7 @@ GUI 是代理 JSON 管理 API 之上的轻量客户端。常用 endpoint 包括 | Endpoint | 用途 | | --- | --- | | `GET` / `PUT /api/settings` | 读取设置或切换 Codex 自动启动。 | +| `GET /api/startup-health` | 读取不含秘密信息的路由、服务、shim 和重启安全诊断。 | | `POST /api/sync` | 重建共享模型目录,并把 Codex 模型缓存标记为过期。 | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 检查、运行和监控自更新任务。 | | `GET` / `PUT /api/sidecar-settings` | 读取或设置 search/vision sidecar 模型。 | diff --git a/gui/src/App.tsx b/gui/src/App.tsx index eb9d5b806a5..19d0a63b9ce 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -10,6 +10,7 @@ import Storage from "./pages/Storage"; import CodexAuth from "./pages/CodexAuth"; import ApiKeys from "./pages/ApiKeys"; import ClaudeCode from "./pages/ClaudeCode"; +import Startup from "./pages/Startup"; import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconKey, IconGithub, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconSparkle, IconX } from "./icons"; import { useI18n, useT, LOCALES, type Locale, type TKey } from "./i18n"; import { Select } from "./ui"; @@ -17,10 +18,10 @@ import { installApiAuthFetch } from "./api"; installApiAuthFetch(); -type Page = "dashboard" | "providers" | "models" | "combos" | "subagents" | "logs" | "usage" | "storage" | "codex-auth" | "api" | "claude"; +type Page = "dashboard" | "startup" | "providers" | "models" | "combos" | "subagents" | "logs" | "usage" | "storage" | "codex-auth" | "api" | "claude"; type Theme = "light" | "dark" | "system"; -const VALID_PAGES = new Set(["dashboard", "providers", "models", "combos", "subagents", "logs", "usage", "storage", "codex-auth", "api", "claude"]); +const VALID_PAGES = new Set(["dashboard", "startup", "providers", "models", "combos", "subagents", "logs", "usage", "storage", "codex-auth", "api", "claude"]); function readPageFromHash(): Page { const raw = location.hash.replace(/^#\/?/, ""); @@ -63,6 +64,7 @@ function providersHashForPage(): string { const NAV: { id: Page; tkey: TKey; Icon: typeof IconGrid }[] = [ { id: "dashboard", tkey: "nav.dashboard", Icon: IconGrid }, + { id: "startup", tkey: "nav.startup", Icon: IconPower }, { id: "providers", tkey: "nav.providers", Icon: IconServer }, { id: "models", tkey: "nav.models", Icon: IconBoxes }, { id: "subagents", tkey: "nav.subagents", Icon: IconBot }, @@ -92,6 +94,7 @@ export default function App() { const [page, setPageState] = useState(readPageFromHash); const [theme, setTheme] = useState(readStoredTheme); const [runtimeVersion, setRuntimeVersion] = useState(null); + const [startupAtRisk, setStartupAtRisk] = useState(false); const { locale, setLocale } = useI18n(); const t = useT(); @@ -135,6 +138,22 @@ export default function App() { return () => window.removeEventListener("hashchange", onHash); }, []); + useEffect(() => { + let cancelled = false; + const readStartupHealth = async () => { + try { + const res = await fetch(`${API_BASE}/api/startup-health`); + if (!res.ok) return; + const data = await res.json() as { status?: unknown }; + if (!cancelled) setStartupAtRisk(data.status === "at-risk"); + } catch { + // The proxy may be stopping; keep the last known lifecycle state. + } + }; + void readStartupHealth(); + return () => { cancelled = true; }; + }, []); + useEffect(() => { const rawHash = window.location.hash.replace(/^#\/?/, ""); // Legacy #debug deep links must resolve before generic normalization @@ -291,6 +310,9 @@ export default function App() { }} aria-current={page === id ? "page" : undefined}> {t(tkey)} + {id === "startup" && startupAtRisk && ( + + )} ))} @@ -330,6 +352,7 @@ export default function App() {
{page === "dashboard" && } + {page === "startup" && } {page === "providers" && } {page === "models" && } {page === "combos" && } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 287b83e9758..01132da1fd2 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1,6 +1,7 @@ // German — generated from en.ts. Must match TKey set (compile-checked). export const de = { "nav.dashboard": "Übersicht", + "nav.startup": "Autostart", "nav.providers": "Anbieter", "nav.models": "Modelle", "nav.combos": "Combos", @@ -19,6 +20,50 @@ export const de = { "theme.dark": "Dunkel", "theme.system": "System", "lang.label": "Sprache", + "startup.title": "Startsicherheit", + "startup.subtitle": "Prüft, ob Codex opencodex nach einem Neustart erreicht, bevor lokales Proxy-Routing in einer Wiederverbindungsschleife endet.", + "startup.refresh": "Aktualisieren", + "startup.loading": "Startschutz wird geprüft…", + "startup.error": "Startschutz konnte nicht gelesen werden.", + "startup.status.native": "Natives Routing", + "startup.status.protected": "Neustartgeschützt", + "startup.status.atRisk": "Aktion erforderlich", + "startup.summary.native": "Codex ist nicht vom lokalen Proxy abhängig", + "startup.summary.protected": "opencodex ist nach einem Neustart verfügbar", + "startup.summary.atRisk": "Codex kann nach einem Neustart den Modellzugriff verlieren", + "startup.riskDetail": "Codex ist auf den lokalen Proxy festgelegt, aber weder ein dauerhafter Dienst noch ein intakter Launcher-Shim startet ihn erneut.", + "startup.riskDetailWindowsShim": "Der Launcher-Shim schützt unterstützte CLI-Skripte, aber Codex Desktop und direkte codex.exe-Aufrufe können ihn unter Windows umgehen.", + "startup.safeDetail": "Routing und Startmechanismus stimmen überein. Nach einem Neustart sollte kein manuelles ocx start nötig sein.", + "startup.routing": "Codex-Routing", + "startup.routing.proxy": "Lokaler Proxy", + "startup.routing.native": "Natives OpenAI", + "startup.restartProtection": "Neustartschutz", + "startup.preference": "Start bei Bedarf", + "startup.enabled": "Aktiviert", + "startup.disabled": "Deaktiviert", + "startup.protection.service": "Hintergrunddienst", + "startup.protection.shim": "Launcher-Shim", + "startup.protection.none": "Nicht installiert", + "startup.details": "Schutzdetails", + "startup.service": "Hintergrunddienst", + "startup.serviceHint": "Startet bei der Anmeldung und startet den Proxy nach einem Absturz neu.", + "startup.installed": "Installiert", + "startup.notInstalled": "Nicht installiert", + "startup.unsupported": "Nicht unterstützt", + "startup.shim": "Codex-Launcher-Shim", + "startup.shimHint": "Führt ocx ensure aus, wenn ein unterstützter Codex-Skript-Launcher startet.", + "startup.healthy": "Intakt", + "startup.cliOnly": "Nur CLI", + "startup.stale": "Veraltet", + "startup.recovery": "Reparaturoptionen", + "startup.recoveryHint": "Führe einen Befehl im Terminal aus. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.", + "startup.command.service": "Empfohlen: dauerhafter Hintergrunddienst", + "startup.command.shim": "Alternative: CLI-Launcher-Shim", + "startup.command.native": "Ausfallsicher: natives Codex-Routing wiederherstellen", + "startup.copy": "Kopieren", + "startup.copied": "Kopiert", + "startup.recommended": "Empfohlene Reparatur: {cmd}", + "startup.navRisk": "Der Startschutz erfordert Aufmerksamkeit", "dash.subtitle": "Live-Status des lokalen opencodex-Proxys, seiner Anbieter und der in Codex gerouteten Modelle.", "dash.status": "Status", "dash.online": "Online", @@ -42,7 +87,7 @@ export const de = { "dash.stopConfirm": "Proxy stoppen und natives Codex wiederherstellen?", "dash.stopping": "Wird gestoppt…", "dash.codexAutoStart": "opencodex mit Codex starten", - "dash.codexAutoStartHint": "Wenn aktiviert, führt das Codex-Shim vor jedem Codex-CLI-/App-Start „ocx ensure\" aus. Deaktiviere dies, um Codex unverändert zu lassen.", + "dash.codexAutoStartHint": "Erlaubt einem installierten Launcher-Shim, ocx ensure auszuführen. Diese Einstellung installiert keinen Neustartschutz; prüfe den effektiven Zustand unter Startsicherheit.", "dash.searchModel": "Such-Sidecar-Modell", "dash.searchModelHint": "Modell für web_search bei nicht über OpenAI gerouteten Modellen. Erfordert ChatGPT-Login.", "dash.searchReasoning": "Such-Reasoning-Aufwand", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 3430c5ba0ba..84f5a2c65d1 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -3,6 +3,7 @@ export const en = { // sidebar / nav / common "nav.dashboard": "Dashboard", + "nav.startup": "Startup", "nav.providers": "Providers", "nav.models": "Models", "nav.combos": "Combos", @@ -27,6 +28,52 @@ export const en = { "theme.system": "System", "lang.label": "Language", + // startup health + "startup.title": "Startup safety", + "startup.subtitle": "Verify that Codex can reach opencodex after a restart, before local proxy routing becomes a reconnect loop.", + "startup.refresh": "Refresh", + "startup.loading": "Checking startup protection…", + "startup.error": "Could not read startup protection.", + "startup.status.native": "Native routing", + "startup.status.protected": "Restart protected", + "startup.status.atRisk": "Action required", + "startup.summary.native": "Codex does not depend on the local proxy", + "startup.summary.protected": "opencodex will be available after restart", + "startup.summary.atRisk": "Codex can lose model access after restart", + "startup.riskDetail": "Codex is pinned to the local proxy, but no persistent service or healthy launcher shim will start it again.", + "startup.riskDetailWindowsShim": "The launcher shim protects supported CLI scripts, but Codex Desktop and direct codex.exe launches can bypass it on Windows.", + "startup.safeDetail": "The current routing and startup mechanism are consistent. No manual ocx start should be required after restart.", + "startup.routing": "Codex routing", + "startup.routing.proxy": "Local proxy", + "startup.routing.native": "Native OpenAI", + "startup.restartProtection": "Restart protection", + "startup.preference": "On-demand startup", + "startup.enabled": "Enabled", + "startup.disabled": "Disabled", + "startup.protection.service": "Background service", + "startup.protection.shim": "Launcher shim", + "startup.protection.none": "Not installed", + "startup.details": "Protection details", + "startup.service": "Background service", + "startup.serviceHint": "Starts at login and restarts the proxy after a crash.", + "startup.installed": "Installed", + "startup.notInstalled": "Not installed", + "startup.unsupported": "Unsupported", + "startup.shim": "Codex launcher shim", + "startup.shimHint": "Runs ocx ensure when a supported Codex script launcher starts.", + "startup.healthy": "Healthy", + "startup.cliOnly": "CLI only", + "startup.stale": "Stale", + "startup.recovery": "Repair options", + "startup.recoveryHint": "Run one command in a terminal. The background service is recommended for Codex Desktop and Windows executables.", + "startup.command.service": "Recommended: persistent background service", + "startup.command.shim": "Alternative: CLI launcher shim", + "startup.command.native": "Fail-safe: restore native Codex routing", + "startup.copy": "Copy", + "startup.copied": "Copied", + "startup.recommended": "Recommended repair: {cmd}", + "startup.navRisk": "Startup protection requires attention", + // dashboard "dash.subtitle": "Live status of the local opencodex proxy, its providers, and the models routed into Codex.", "dash.status": "Status", @@ -51,7 +98,7 @@ export const en = { "dash.stopConfirm": "Stop the proxy and restore native Codex?", "dash.stopping": "Stopping…", "dash.codexAutoStart": "Start opencodex with Codex", - "dash.codexAutoStartHint": "When enabled, the Codex shim runs ocx ensure before every Codex CLI/App launch. Disable this to leave Codex untouched.", + "dash.codexAutoStartHint": "Allows an installed launcher shim to run ocx ensure. This setting does not install restart protection; check Startup safety for the effective state.", "dash.searchModel": "Search sidecar model", "dash.searchModelHint": "Model used for web_search on non-OpenAI routed models. Requires ChatGPT login.", "dash.searchReasoning": "Search reasoning effort", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 0babac0bffc..40cfca20ab4 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -3,6 +3,7 @@ import type { TKey } from "./en"; export const ja: Record = { // sidebar / nav / common "nav.dashboard": "ダッシュボード", + "nav.startup": "起動安全性", "nav.providers": "プロバイダー", "nav.models": "モデル", "nav.combos": "コンボ", @@ -27,6 +28,52 @@ export const ja: Record = { "theme.system": "システム", "lang.label": "言語", + // startup health + "startup.title": "起動安全性", + "startup.subtitle": "再起動後にローカルプロキシへの接続が再接続ループになる前に、Codex が opencodex へ到達できるか確認します。", + "startup.refresh": "更新", + "startup.loading": "起動保護を確認中…", + "startup.error": "起動保護を読み取れませんでした。", + "startup.status.native": "ネイティブルーティング", + "startup.status.protected": "再起動保護済み", + "startup.status.atRisk": "対応が必要", + "startup.summary.native": "Codex はローカルプロキシに依存していません", + "startup.summary.protected": "再起動後も opencodex を利用できます", + "startup.summary.atRisk": "再起動後に Codex がモデルへ接続できなくなる可能性があります", + "startup.riskDetail": "Codex はローカルプロキシを参照していますが、再起動する永続サービスまたは正常な launcher shim がありません。", + "startup.riskDetailWindowsShim": "Launcher shim は対応する CLI スクリプトのみを保護し、Windows の Codex Desktop と codex.exe の直接起動はこれを迂回できます。", + "startup.safeDetail": "現在のルーティングと起動方式は整合しています。再起動後に ocx start を手動実行する必要はありません。", + "startup.routing": "Codex ルーティング", + "startup.routing.proxy": "ローカルプロキシ", + "startup.routing.native": "OpenAI ネイティブ", + "startup.restartProtection": "再起動保護", + "startup.preference": "オンデマンド起動", + "startup.enabled": "有効", + "startup.disabled": "無効", + "startup.protection.service": "バックグラウンドサービス", + "startup.protection.shim": "Launcher shim", + "startup.protection.none": "未インストール", + "startup.details": "保護の詳細", + "startup.service": "バックグラウンドサービス", + "startup.serviceHint": "ログイン時に起動し、クラッシュ後にプロキシを再起動します。", + "startup.installed": "インストール済み", + "startup.notInstalled": "未インストール", + "startup.unsupported": "未対応", + "startup.shim": "Codex launcher shim", + "startup.shimHint": "対応する Codex スクリプトランチャーの起動時に ocx ensure を実行します。", + "startup.healthy": "正常", + "startup.cliOnly": "CLI のみ", + "startup.stale": "破損", + "startup.recovery": "修復方法", + "startup.recoveryHint": "ターミナルでコマンドを1つ実行します。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。", + "startup.command.service": "推奨: 永続バックグラウンドサービス", + "startup.command.shim": "代替: CLI launcher shim", + "startup.command.native": "安全策: Codex ネイティブルーティングを復元", + "startup.copy": "コピー", + "startup.copied": "コピー済み", + "startup.recommended": "推奨修復: {cmd}", + "startup.navRisk": "起動保護に対応が必要です", + // dashboard "dash.subtitle": "ローカル opencodex プロキシ、そのプロバイダー、Codex にルーティングされるモデルのライブ状態です。", "dash.status": "状態", @@ -51,7 +98,7 @@ export const ja: Record = { "dash.stopConfirm": "プロキシを停止してネイティブの Codex に戻しますか?", "dash.stopping": "停止中…", "dash.codexAutoStart": "Codex と一緒に opencodex を起動", - "dash.codexAutoStartHint": "オンにすると、Codex シムは毎回の Codex CLI/App 起動前に ocx ensure を実行します。オフにすると Codex はそのまま変更されません。", + "dash.codexAutoStartHint": "インストール済み launcher shim に ocx ensure の実行を許可します。この設定だけでは再起動保護はインストールされません。起動安全性で実際の状態を確認してください。", "dash.searchModel": "検索サイドカーモデル", "dash.searchModelHint": "非 OpenAI ルーティングモデルで web_search に使うモデル。ChatGPT ログインが必要です。", "dash.searchReasoning": "検索の推論負荷", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index acbaf9750a4..8bd8b2c7d6a 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -3,6 +3,7 @@ import type { TKey } from "./en"; export const ko: Record = { // sidebar / nav / common "nav.dashboard": "대시보드", + "nav.startup": "시작 안전성", "nav.providers": "프로바이더", "nav.models": "모델", "nav.combos": "콤보", @@ -22,6 +23,52 @@ export const ko: Record = { "theme.system": "시스템", "lang.label": "언어", + // startup health + "startup.title": "시작 안전성", + "startup.subtitle": "재부팅 후 로컬 프록시 라우팅이 재연결 반복으로 이어지기 전에 Codex가 opencodex에 연결될 수 있는지 확인합니다.", + "startup.refresh": "새로고침", + "startup.loading": "시작 보호 상태 확인 중…", + "startup.error": "시작 보호 상태를 읽지 못했습니다.", + "startup.status.native": "네이티브 라우팅", + "startup.status.protected": "재부팅 보호됨", + "startup.status.atRisk": "조치 필요", + "startup.summary.native": "Codex가 로컬 프록시에 의존하지 않습니다", + "startup.summary.protected": "재부팅 후에도 opencodex가 자동으로 준비됩니다", + "startup.summary.atRisk": "재부팅 후 Codex 모델 연결이 끊길 수 있습니다", + "startup.riskDetail": "Codex는 로컬 프록시를 바라보지만 이를 다시 시작할 영구 서비스나 정상 launcher shim이 없습니다.", + "startup.riskDetailWindowsShim": "Launcher shim은 지원되는 CLI 스크립트만 보호하며 Windows의 Codex Desktop과 직접 codex.exe 실행은 이를 우회할 수 있습니다.", + "startup.safeDetail": "현재 라우팅과 시작 방식이 일치합니다. 재부팅 후 ocx start를 수동으로 실행할 필요가 없습니다.", + "startup.routing": "Codex 라우팅", + "startup.routing.proxy": "로컬 프록시", + "startup.routing.native": "OpenAI 네이티브", + "startup.restartProtection": "재부팅 보호", + "startup.preference": "필요 시 자동 시작", + "startup.enabled": "켜짐", + "startup.disabled": "꺼짐", + "startup.protection.service": "백그라운드 서비스", + "startup.protection.shim": "Launcher shim", + "startup.protection.none": "설치되지 않음", + "startup.details": "보호 상태 상세", + "startup.service": "백그라운드 서비스", + "startup.serviceHint": "로그인할 때 시작하고 프록시가 중단되면 다시 실행합니다.", + "startup.installed": "설치됨", + "startup.notInstalled": "설치되지 않음", + "startup.unsupported": "지원되지 않음", + "startup.shim": "Codex launcher shim", + "startup.shimHint": "지원되는 Codex 스크립트 런처가 시작될 때 ocx ensure를 실행합니다.", + "startup.healthy": "정상", + "startup.cliOnly": "CLI 전용", + "startup.stale": "손상됨", + "startup.recovery": "복구 방법", + "startup.recoveryHint": "터미널에서 명령 하나를 실행하세요. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.", + "startup.command.service": "권장: 영구 백그라운드 서비스", + "startup.command.shim": "대안: CLI launcher shim", + "startup.command.native": "안전 전환: Codex 네이티브 라우팅 복구", + "startup.copy": "복사", + "startup.copied": "복사됨", + "startup.recommended": "권장 복구 명령: {cmd}", + "startup.navRisk": "시작 보호 상태에 조치가 필요합니다", + // dashboard "dash.subtitle": "로컬 opencodex 프록시와 프로바이더, 그리고 Codex로 라우팅되는 모델의 실시간 상태입니다.", "dash.status": "상태", @@ -46,7 +93,7 @@ export const ko: Record = { "dash.stopConfirm": "프록시를 중지하고 Codex 원본 설정을 복원할까요?", "dash.stopping": "중지 중…", "dash.codexAutoStart": "Codex 실행 시 opencodex 시작", - "dash.codexAutoStartHint": "켜져 있으면 Codex CLI/App 실행 전 shim이 ocx ensure를 실행합니다. 끄면 Codex를 건드리지 않습니다.", + "dash.codexAutoStartHint": "설치된 launcher shim이 ocx ensure를 실행하도록 허용합니다. 이 설정은 재부팅 보호를 설치하지 않으므로 시작 안전성에서 실제 상태를 확인하세요.", "dash.searchModel": "서치 사이드카 모델", "dash.searchModelHint": "비-OpenAI 라우팅 모델의 web_search에 사용되는 모델입니다. ChatGPT 로그인 필요.", "dash.searchReasoning": "서치 추론 강도", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index a4872452021..11aa35826f6 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -3,6 +3,7 @@ import type { TKey } from "./en"; export const ru: Record = { // sidebar / nav / common "nav.dashboard": "Дашборд", + "nav.startup": "Автозапуск", "nav.providers": "Провайдеры", "nav.models": "Модели", "nav.combos": "Комбо", @@ -27,6 +28,52 @@ export const ru: Record = { "theme.system": "Системная", "lang.label": "Язык", + // startup health + "startup.title": "Безопасность запуска", + "startup.subtitle": "Проверьте, сможет ли Codex подключиться к opencodex после перезагрузки, прежде чем локальный прокси вызовет бесконечное переподключение.", + "startup.refresh": "Обновить", + "startup.loading": "Проверка защиты запуска…", + "startup.error": "Не удалось прочитать состояние защиты запуска.", + "startup.status.native": "Нативная маршрутизация", + "startup.status.protected": "Перезапуск защищён", + "startup.status.atRisk": "Требуется действие", + "startup.summary.native": "Codex не зависит от локального прокси", + "startup.summary.protected": "opencodex будет доступен после перезагрузки", + "startup.summary.atRisk": "После перезагрузки Codex может потерять доступ к моделям", + "startup.riskDetail": "Codex направлен на локальный прокси, но постоянная служба или исправный launcher shim не запустят его снова.", + "startup.riskDetailWindowsShim": "Launcher shim защищает поддерживаемые CLI-скрипты, но Codex Desktop и прямой запуск codex.exe в Windows могут обходить его.", + "startup.safeDetail": "Маршрутизация и механизм запуска согласованы. После перезагрузки ручной запуск ocx start не требуется.", + "startup.routing": "Маршрутизация Codex", + "startup.routing.proxy": "Локальный прокси", + "startup.routing.native": "Нативный OpenAI", + "startup.restartProtection": "Защита перезапуска", + "startup.preference": "Запуск по требованию", + "startup.enabled": "Включён", + "startup.disabled": "Выключен", + "startup.protection.service": "Фоновая служба", + "startup.protection.shim": "Launcher shim", + "startup.protection.none": "Не установлен", + "startup.details": "Сведения о защите", + "startup.service": "Фоновая служба", + "startup.serviceHint": "Запускается при входе и перезапускает прокси после сбоя.", + "startup.installed": "Установлена", + "startup.notInstalled": "Не установлена", + "startup.unsupported": "Не поддерживается", + "startup.shim": "Codex launcher shim", + "startup.shimHint": "Запускает ocx ensure при запуске поддерживаемого скриптового лаунчера Codex.", + "startup.healthy": "Исправен", + "startup.cliOnly": "Только CLI", + "startup.stale": "Повреждён", + "startup.recovery": "Варианты исправления", + "startup.recoveryHint": "Выполните одну команду в терминале. Для Codex Desktop и Windows рекомендуется фоновая служба.", + "startup.command.service": "Рекомендуется: постоянная фоновая служба", + "startup.command.shim": "Альтернатива: CLI launcher shim", + "startup.command.native": "Безопасный режим: восстановить нативную маршрутизацию Codex", + "startup.copy": "Копировать", + "startup.copied": "Скопировано", + "startup.recommended": "Рекомендуемое исправление: {cmd}", + "startup.navRisk": "Защита запуска требует внимания", + // dashboard "dash.subtitle": "Актуальное состояние локального прокси opencodex, его провайдеров и моделей, маршрутизируемых в Codex.", "dash.status": "Статус", @@ -51,7 +98,7 @@ export const ru: Record = { "dash.stopConfirm": "Остановить прокси и восстановить нативный Codex?", "dash.stopping": "Остановка…", "dash.codexAutoStart": "Запускать opencodex вместе с Codex", - "dash.codexAutoStartHint": "Когда параметр включён, обёртка (shim) Codex выполняет ocx ensure перед каждым запуском Codex CLI/App. Отключите его, чтобы оставить Codex нетронутым.", + "dash.codexAutoStartHint": "Разрешает установленному launcher shim выполнять ocx ensure. Эта настройка не устанавливает защиту перезапуска; проверьте фактическое состояние в разделе безопасности запуска.", "dash.searchModel": "Модель сайдкара поиска", "dash.searchModelHint": "Модель, используемая для web_search на маршрутизируемых моделях, отличных от OpenAI. Требуется вход в аккаунт ChatGPT.", "dash.searchReasoning": "Уровень рассуждений для поиска", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index d3ebea74206..7708ace6db6 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -3,6 +3,7 @@ import type { TKey } from "./en"; export const zh: Record = { // sidebar / nav / common "nav.dashboard": "仪表盘", + "nav.startup": "启动安全", "nav.providers": "提供方", "nav.models": "模型", "nav.combos": "组合", @@ -22,6 +23,52 @@ export const zh: Record = { "theme.system": "跟随系统", "lang.label": "语言", + // startup health + "startup.title": "启动安全", + "startup.subtitle": "检查重启后 Codex 是否仍能连接 opencodex,避免本地代理路由陷入重复重连。", + "startup.refresh": "刷新", + "startup.loading": "正在检查启动保护…", + "startup.error": "无法读取启动保护状态。", + "startup.status.native": "原生路由", + "startup.status.protected": "已保护重启", + "startup.status.atRisk": "需要处理", + "startup.summary.native": "Codex 不依赖本地代理", + "startup.summary.protected": "重启后 opencodex 会自动可用", + "startup.summary.atRisk": "重启后 Codex 可能无法访问模型", + "startup.riskDetail": "Codex 已指向本地代理,但没有持久服务或正常的 launcher shim 将其重新启动。", + "startup.riskDetailWindowsShim": "Launcher shim 仅保护受支持的 CLI 脚本;Windows 上的 Codex Desktop 和直接 codex.exe 启动可以绕过它。", + "startup.safeDetail": "当前路由与启动机制一致。重启后无需手动运行 ocx start。", + "startup.routing": "Codex 路由", + "startup.routing.proxy": "本地代理", + "startup.routing.native": "OpenAI 原生", + "startup.restartProtection": "重启保护", + "startup.preference": "按需启动", + "startup.enabled": "已启用", + "startup.disabled": "已禁用", + "startup.protection.service": "后台服务", + "startup.protection.shim": "Launcher shim", + "startup.protection.none": "未安装", + "startup.details": "保护详情", + "startup.service": "后台服务", + "startup.serviceHint": "登录时启动,并在代理崩溃后重新启动。", + "startup.installed": "已安装", + "startup.notInstalled": "未安装", + "startup.unsupported": "不支持", + "startup.shim": "Codex launcher shim", + "startup.shimHint": "支持的 Codex 脚本启动器运行时执行 ocx ensure。", + "startup.healthy": "正常", + "startup.cliOnly": "仅 CLI", + "startup.stale": "已失效", + "startup.recovery": "修复选项", + "startup.recoveryHint": "在终端运行一个命令。Codex Desktop 和 Windows 可执行文件建议使用后台服务。", + "startup.command.service": "推荐:持久后台服务", + "startup.command.shim": "备选:CLI launcher shim", + "startup.command.native": "安全恢复:还原 Codex 原生路由", + "startup.copy": "复制", + "startup.copied": "已复制", + "startup.recommended": "推荐修复:{cmd}", + "startup.navRisk": "启动保护需要处理", + // dashboard "dash.subtitle": "本地 opencodex 代理、其提供方以及路由到 Codex 的模型的实时状态。", "dash.status": "状态", @@ -46,7 +93,7 @@ export const zh: Record = { "dash.stopConfirm": "停止代理并恢复原生 Codex 配置?", "dash.stopping": "正在停止…", "dash.codexAutoStart": "随 Codex 启动 opencodex", - "dash.codexAutoStartHint": "启用后,Codex shim 会在每次 Codex CLI/App 启动前运行 ocx ensure。关闭后不会改动 Codex。", + "dash.codexAutoStartHint": "允许已安装的 launcher shim 运行 ocx ensure。此设置不会安装重启保护;请在启动安全中检查实际状态。", "dash.searchModel": "搜索附属模型", "dash.searchModelHint": "用于非 OpenAI 路由模型的 web_search 的模型。需要 ChatGPT 登录。", "dash.searchReasoning": "搜索推理强度", diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index 2d2bc5d1462..53e9c917036 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -9,7 +9,16 @@ import { EmptyState, Select } from "../ui"; interface HealthData { status: string; version: string; uptime: number } interface ProviderInfo { name: string; adapter: string; baseUrl: string; defaultModel?: string; hasApiKey: boolean } interface ModelInfo { id: string; provider: string; owned_by?: string } -interface SettingsData { codexAutoStart: boolean; port: number; hostname: string } +interface SettingsData { + codexAutoStart: boolean; + port: number; + hostname: string; + startupHealth?: { + status: "native" | "protected" | "at-risk"; + autostartEnabled: boolean; + shimCoverage: "full" | "cli-only" | "none"; + }; +} type SidecarBackend = "openai" | "anthropic"; interface SidecarSetting { backend?: SidecarBackend; model: string } interface SidecarData { webSearch: SidecarSetting; vision: SidecarSetting } @@ -446,8 +455,8 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { body: JSON.stringify({ codexAutoStart: next }), }); if (!res.ok) throw new Error("save failed"); - const data = await res.json(); - setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart } : prev); + const data = await res.json() as { codexAutoStart: boolean; startupHealth?: SettingsData["startupHealth"] }; + setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); } catch { setSettings(prev => prev ? { ...prev, codexAutoStart: !next } : prev); setError(true); @@ -617,6 +626,16 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
+ {settings?.startupHealth?.status === "at-risk" && ( +
+ + + {t(settings.startupHealth.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail")}{" "} + {t("startup.title")} + +
+ )} + {projectConfigWarnings.length > 0 && (
diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx new file mode 100644 index 00000000000..427a9b8b727 --- /dev/null +++ b/gui/src/pages/Startup.tsx @@ -0,0 +1,211 @@ +import { useCallback, useEffect, useState } from "react"; +import { IconAlert, IconCheck, IconPower, IconRefresh, IconTerminal } from "../icons"; +import { useI18n, type TKey } from "../i18n/shared"; +import { EmptyState } from "../ui"; + +type StartupStatus = "native" | "protected" | "at-risk"; +type StartupProtection = "service" | "shim" | "none"; + +interface StartupHealthData { + status: StartupStatus; + routingInjected: boolean; + autostartEnabled: boolean; + rebootSafe: boolean; + protection: StartupProtection; + serviceInstalled: boolean; + serviceSupported: boolean; + shimInstalled: boolean; + shimHealthy: boolean; + shimCoverage: "full" | "cli-only" | "none"; + platform: string; + recommendedCommand: string | null; + commands: { + installService: string; + installShim: string; + restoreNative: string; + }; +} + +const STATUS_KEYS: Record = { + native: "startup.status.native", + protected: "startup.status.protected", + "at-risk": "startup.status.atRisk", +}; + +const SUMMARY_KEYS: Record = { + native: "startup.summary.native", + protected: "startup.summary.protected", + "at-risk": "startup.summary.atRisk", +}; + +const PROTECTION_KEYS: Record = { + service: "startup.protection.service", + shim: "startup.protection.shim", + none: "startup.protection.none", +}; + +function StateBadge({ ok, yes, no }: { ok: boolean; yes: string; no: string }) { + return {ok ? yes : no}; +} + +export default function Startup({ apiBase }: { apiBase: string }) { + const { t } = useI18n(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [failed, setFailed] = useState(false); + const [copied, setCopied] = useState(null); + + const refresh = useCallback(async (signal?: AbortSignal) => { + setLoading(true); + try { + const res = await fetch(`${apiBase}/api/startup-health`, { signal }); + if (!res.ok) throw new Error("fetch failed"); + const next = await res.json() as StartupHealthData; + if (signal?.aborted) return; + setData(next); + setFailed(false); + } catch { + if (signal?.aborted) return; + setFailed(true); + } finally { + if (!signal?.aborted) setLoading(false); + } + }, [apiBase]); + + useEffect(() => { + const controller = new AbortController(); + const timer = window.setTimeout(() => { void refresh(controller.signal); }, 0); + return () => { + window.clearTimeout(timer); + controller.abort(); + }; + }, [refresh]); + + const copyCommand = async (command: string) => { + try { + await navigator.clipboard.writeText(command); + setCopied(command); + window.setTimeout(() => setCopied(current => current === command ? null : current), 1600); + } catch { + setCopied(null); + } + }; + + const statusClass = data?.status === "protected" + ? "startup-hero--safe" + : data?.status === "at-risk" + ? "startup-hero--risk" + : "startup-hero--native"; + const StatusIcon = data?.status === "at-risk" ? IconAlert : IconCheck; + + return ( + <> +
+
+

{t("startup.title")}

+

{t("startup.subtitle")}

+
+ +
+ + {loading && !data ? ( + + ) : failed && !data ? ( + + ) : data ? ( + <> +
+
+
+ + {t(STATUS_KEYS[data.status])} + +

{t(SUMMARY_KEYS[data.status])}

+

{data.status === "at-risk" + ? t(data.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail") + : t("startup.safeDetail")}

+
+
+ +
+
+
{t("startup.routing")}
+
{t(data.routingInjected ? "startup.routing.proxy" : "startup.routing.native")}
+
+
+
{t("startup.restartProtection")}
+
{t(PROTECTION_KEYS[data.protection])}
+
+
+
{t("startup.preference")}
+
{t(data.autostartEnabled ? "startup.enabled" : "startup.disabled")}
+
+
+ +
+
+

{t("startup.details")}

+ {data.platform} +
+
+
{t("startup.service")}{t("startup.serviceHint")}
+ +
+
+
{t("startup.shim")}{t("startup.shimHint")}
+ +
+
+ +
+
+

{t("startup.recovery")}

+ +
+

{t("startup.recoveryHint")}

+
+
+
+ {t("startup.command.service")} + {data.commands.installService} +
+ +
+
+
+ {t("startup.command.shim")} + {data.commands.installShim} +
+ +
+
+
+ {t("startup.command.native")} + {data.commands.restoreNative} +
+ +
+
+ {data.status === "at-risk" && ( +
+ {t("startup.recommended", { cmd: data.recommendedCommand ?? data.commands.installService })} +
+ )} +
+ + ) : null} + + ); +} diff --git a/gui/src/styles.css b/gui/src/styles.css index ded0fbfe1be..774af239622 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -708,6 +708,44 @@ dialog.modal-overlay::backdrop { .toggle.on .toggle-knob { left: 17px; background: var(--toggle-dot-color); } .notice-warn { font-size: var(--text-label); line-height: var(--leading-body); padding: 8px 10px; border-radius: var(--radius-sm); background: var(--amber-soft); color: var(--amber); display: flex; align-items: center; gap: 6px; margin-bottom: 12px; } .notice-warn svg { width: 14px; height: 14px; flex-shrink: 0; } + +/* Startup lifecycle health — compact infrastructure status, not a setup wizard. */ +.nav-health-dot { + width: 8px; + height: 8px; + border-radius: var(--radius-round); + background: var(--amber); + box-shadow: 0 0 0 3px var(--amber-soft); + margin-left: auto; + flex: 0 0 auto; +} +.startup-page-sub { margin-bottom: 0; } +.startup-hero { display: flex; gap: 16px; align-items: flex-start; margin-bottom: 16px; } +.startup-hero--safe { border-color: color-mix(in srgb, var(--green) 34%, var(--border)); background: color-mix(in srgb, var(--green-soft) 64%, var(--surface)); } +.startup-hero--risk { border-color: color-mix(in srgb, var(--amber) 40%, var(--border)); background: color-mix(in srgb, var(--amber-soft) 72%, var(--surface)); } +.startup-hero--native { border-color: color-mix(in srgb, var(--accent) 20%, var(--border)); } +.startup-hero-icon { width: 42px; height: 42px; border-radius: var(--radius); display: grid; place-items: center; background: var(--raised); flex: 0 0 auto; } +.startup-hero-icon svg { width: 21px; height: 21px; } +.startup-hero-copy h3 { margin: 10px 0 4px; font-size: var(--text-title); } +.startup-hero-copy p { margin: 0; color: var(--muted); line-height: var(--leading-body); } +.startup-state-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } +.startup-state-grid .value { font-size: var(--text-subtitle); } +.startup-details, .startup-actions { margin-bottom: 16px; } +.startup-detail-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid var(--border-soft); } +.startup-detail-row > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.startup-detail-row span:not(.badge) { color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); } +.startup-actions > .muted { margin: -4px 0 14px; font-size: var(--text-control); } +.startup-command-list { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); overflow: hidden; } +.startup-command-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px; } +.startup-command-row + .startup-command-row { border-top: 1px solid var(--border-soft); } +.startup-command-row > div { display: flex; flex-direction: column; gap: 5px; min-width: 0; } +.startup-command-row strong { font-size: var(--text-control); } +.startup-command-row code { color: var(--muted); font-size: var(--text-label); overflow-wrap: anywhere; } +.startup-action-notice { margin: 14px 0 0; } +@media (max-width: 700px) { + .startup-state-grid { grid-template-columns: 1fr; } + .startup-command-row { align-items: flex-start; } +} .modal-desc { font-size: var(--text-control); line-height: var(--leading-body); color: var(--muted); margin-bottom: 14px; } .modal-actions { display: flex; gap: 8px; margin-top: 16px; } .modal-actions .btn { flex: 1; } diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 2eebfd6de79..a5fe2d7a16c 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -16,6 +16,7 @@ import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWi import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim"; import { countPendingOpencodexHistory } from "../codex/history-provider"; import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings"; +import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health"; export { resolveCodexHomeDir } from "../codex/home"; const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"; @@ -331,6 +332,11 @@ export async function runDoctor(): Promise { console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`); } + const startup = collectStartupHealth(readConfigDiagnostics().config); + console.log("\nCodex restart safety"); + console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`); + console.log(` routing=${startup.routingInjected ? "opencodex" : "native"}, service=${startup.serviceInstalled ? "installed" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`); + const currentProxyEnv = collectProxyEnv(); const configuredProxy = collectConfiguredProxy(); const runningProxyEnv = collectRunningProxyEnv(); @@ -403,6 +409,9 @@ export async function runDoctor(): Promise { const hints: string[] = []; const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive); const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present; + if (!startup.rebootSafe) { + hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${startup.commands.installService}' or restore native routing with '${startup.commands.restoreNative}'.`); + } if (anyDrvfs) { hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability."); } diff --git a/src/cli/index.ts b/src/cli/index.ts index bf1b7615865..2a1571b5606 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -27,6 +27,7 @@ import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-li import { stopProxy } from "../lib/process-control"; import { loadServiceTokenFromFile } from "../lib/service-secrets"; import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service"; +import { startupHealthSummary } from "../codex/autostart-health"; import { drainAndShutdown, startServer } from "../server"; import { injectSystemEnv, revertSystemEnv } from "../server/system-env"; import { buildDesktop3pRegistry } from "../claude/desktop-3p"; @@ -448,6 +449,7 @@ async function handleStatus() { console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`); console.log(` Default provider: ${status.json.defaultProvider}`); console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`); + console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`); console.log(` Service: ${status.json.service.summary}`); console.log(` ${status.json.codexShim.summary}`); if (status.json.codexPlugins.applicable) { diff --git a/src/cli/status.ts b/src/cli/status.ts index 04a5bddcd60..c6973dce2be 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -4,6 +4,9 @@ import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../cod import { isOpencodexHealthz, probeHostname } from "../server/proxy-liveness"; import type { OcxConfig } from "../types"; import { serviceStatusSummary } from "../service"; +import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health"; +import { isCodexRoutingInjected } from "../codex/inject"; +import { diagnoseCodexShim } from "../codex/shim"; type HealthCheck = { ok: boolean; @@ -39,6 +42,7 @@ export type CliStatusJson = { overrideEnv?: string; }; codexAutostart: boolean; + startup: StartupHealth; defaultProvider: string | null; config: { source: "default" | "file" | "fallback"; @@ -116,8 +120,17 @@ export async function collectStatus(): Promise { const health = await checkProxyHealth(listen); const bunRuntime = durableBunRuntime(); const serviceSummary = serviceStatusSummary(); - const { codexShimStatus } = await import("../codex/shim"); - const codexShimSummary = codexShimStatus(); + const codexShim = diagnoseCodexShim(); + const codexShimSummary = codexShim.summary; + const startup = deriveStartupHealth({ + routingInjected: isCodexRoutingInjected(), + autostartEnabled: codexAutoStartEnabled(config), + serviceInstalled: serviceSummary.startsWith("installed"), + serviceSupported: !serviceSummary.startsWith("unsupported"), + shimInstalled: codexShim.installed, + shimHealthy: codexShim.healthy, + platform: process.platform, + }); const codexPlugins = diagnoseCodexBundledPlugins(); const proxyLabel = pid && health.ok ? `running (PID ${pid})` @@ -157,6 +170,7 @@ export async function collectStatus(): Promise { ...(bunRuntime.source === "override" ? { overrideEnv: bunRuntime.overrideEnv } : {}), }, codexAutostart: codexAutoStartEnabled(config), + startup, defaultProvider: typeof config.defaultProvider === "string" ? config.defaultProvider : null, config: { source: configDiagnostics.source, diff --git a/src/codex/autostart-health.ts b/src/codex/autostart-health.ts new file mode 100644 index 00000000000..82a3354abcd --- /dev/null +++ b/src/codex/autostart-health.ts @@ -0,0 +1,107 @@ +import { codexAutoStartEnabled } from "../config"; +import { serviceStatusSummary } from "../service"; +import type { OcxConfig } from "../types"; +import { isCodexRoutingInjected } from "./inject"; +import { diagnoseCodexShim } from "./shim"; + +export type StartupProtection = "service" | "shim" | "none"; +export type StartupHealthStatus = "native" | "protected" | "at-risk"; +export type ShimCoverage = "full" | "cli-only" | "none"; + +export interface StartupHealthInputs { + routingInjected: boolean; + autostartEnabled: boolean; + serviceInstalled: boolean; + serviceSupported: boolean; + shimInstalled: boolean; + shimHealthy: boolean; + platform: NodeJS.Platform; +} + +export interface StartupHealth { + status: StartupHealthStatus; + routingInjected: boolean; + autostartEnabled: boolean; + rebootSafe: boolean; + protection: StartupProtection; + serviceInstalled: boolean; + shimInstalled: boolean; + shimHealthy: boolean; + shimCoverage: ShimCoverage; + serviceSupported: boolean; + platform: NodeJS.Platform; + recommendedCommand: string | null; + commands: { + installService: string; + installShim: string; + restoreNative: string; + }; +} + +const COMMANDS = { + installService: "ocx service install", + installShim: "ocx codex-shim install", + restoreNative: "ocx restore", +} as const; + +export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth { + const shimEffective = inputs.autostartEnabled && inputs.shimHealthy; + // Windows Codex Desktop and exact codex.exe invocations bypass script wrappers. + // A healthy shim protects supported CLI launchers there, but not the global + // openai_base_url consumed by every Codex surface. + const shimCoverage: ShimCoverage = !shimEffective + ? "none" + : inputs.platform === "win32" + ? "cli-only" + : "full"; + const protection: StartupProtection = inputs.serviceInstalled + ? "service" + : shimEffective + ? "shim" + : "none"; + const rebootSafe = !inputs.routingInjected || inputs.serviceInstalled || shimCoverage === "full"; + const status: StartupHealthStatus = !inputs.routingInjected + ? "native" + : rebootSafe + ? "protected" + : "at-risk"; + const recommendedCommand = status !== "at-risk" + ? null + : inputs.serviceSupported + ? COMMANDS.installService + : inputs.autostartEnabled + ? COMMANDS.installShim + : COMMANDS.restoreNative; + return { + ...inputs, + status, + rebootSafe, + protection, + shimCoverage, + recommendedCommand, + commands: { ...COMMANDS }, + }; +} + +/** Collect current machine state without mutating config, services, or shims. */ +export function collectStartupHealth(config: Pick): StartupHealth { + const shim = diagnoseCodexShim(); + const serviceSummary = serviceStatusSummary(); + return deriveStartupHealth({ + routingInjected: isCodexRoutingInjected(), + autostartEnabled: codexAutoStartEnabled(config), + serviceInstalled: serviceSummary.startsWith("installed"), + serviceSupported: !serviceSummary.startsWith("unsupported"), + shimInstalled: shim.installed, + shimHealthy: shim.healthy, + platform: process.platform, + }); +} + +export function startupHealthSummary(health: StartupHealth): string { + if (health.status === "native") return "native Codex routing (no opencodex restart dependency)"; + if (health.protection === "service") return "protected by background service"; + if (health.shimCoverage === "full") return "protected by Codex launcher shim"; + if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + return `AT RISK after restart (no service or healthy shim; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; +} diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 07166a38636..aeba43cd181 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -151,7 +151,7 @@ export function stripInjectedOpenaiBaseUrl(content: string): string { return lines.filter((_, i) => !drop.has(i)).join("\n"); } -function hasInjectedOpenaiBaseUrl(content: string): boolean { +export function hasInjectedOpenaiBaseUrl(content: string): boolean { const lines = content.split("\n"); const firstTable = lines.findIndex(l => /^\s*\[/.test(l)); const rootEnd = firstTable === -1 ? lines.length : firstTable; @@ -161,6 +161,28 @@ function hasInjectedOpenaiBaseUrl(content: string): boolean { return false; } +/** + * True when the active Codex config is owned by opencodex routing. Covers the + * loopback Design B root override and the legacy/non-loopback provider table. + * A user-owned `openai_base_url` is intentionally not classified as injected. + */ +export function hasInjectedCodexRouting(content: string): boolean { + if (hasInjectedOpenaiBaseUrl(content)) return true; + return /^\s*model_provider\s*=\s*["']opencodex["']\s*$/m.test(content) + && /^\s*\[model_providers\.opencodex\]\s*$/m.test(content); +} + +/** Read-only probe used by status, doctor, and the dashboard. */ +export function isCodexRoutingInjected(): boolean { + const path = CODEX_CONFIG_PATH; + if (!existsSync(path)) return false; + try { + return hasInjectedCodexRouting(readFileSync(path, "utf8")); + } catch { + return false; + } +} + /** * Strip every existing `model_provider` line that we must not duplicate: any line set to * "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any diff --git a/src/codex/shim.ts b/src/codex/shim.ts index de5756c4631..e418d3d48e4 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -504,13 +504,32 @@ export function uninstallCodexShim(): { removed: boolean; message: string } { /** True if a Codex autostart shim is currently installed (state file present). */ export function isCodexShimInstalled(): boolean { - return readState() !== null; + return diagnoseCodexShim().installed; } -export function codexShimStatus(): string { +export interface CodexShimDiagnostic { + installed: boolean; + healthy: boolean; + summary: string; +} + +/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */ +export function diagnoseCodexShim(): CodexShimDiagnostic { const state = readState(); - if (!state) return "Codex autostart shim is not installed."; - return stateFiles(state).map(file => { + if (!state) { + return { + installed: false, + healthy: false, + summary: "Codex autostart shim is not installed.", + }; + } + const files = stateFiles(state); + const healthy = files.length > 0 && files.every(file => file.preserveOnly + ? existsSync(file.backupPath) && !existsSync(file.originalPath) + : existsSync(file.wrapperPath) + && (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false)) + && isShim(file.wrapperPath)); + const summary = files.map(file => { const wrapper = existsSync(file.wrapperPath) ? isShim(file.wrapperPath) ? "shim present" @@ -519,4 +538,9 @@ export function codexShimStatus(): string { const backup = existsSync(file.backupPath) ? "present" : "missing"; return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`; }).join("\n"); + return { installed: true, healthy, summary }; +} + +export function codexShimStatus(): string { + return diagnoseCodexShim().summary; } diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 678d0cf5f0c..08cab23e228 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -55,6 +55,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; import { applySystemEnvToggle } from "../system-env"; +import { collectStartupHealth } from "../../codex/autostart-health"; import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared"; import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; @@ -75,9 +76,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise`. | Endpoint area | Responsibility | | --- | --- | | Config/settings | Read safe config/settings views; mutate supported settings only. Full `PUT /api/config` is disabled so masked secrets are not round-tripped. | +| Startup safety | `GET /api/startup-health` reports whether injected Codex routing is restart-safe, with secret-free service/shim diagnostics. On Windows a healthy script shim is CLI-only; Codex Desktop requires the background service for full protection. | | Providers | Create/update/delete ordinary provider configs and enrich registry metadata. The reserved `openai` card exposes Pool(default)/Direct account mode; `openai-apikey` remains the separate API route. | | Models | Fetch routed model lists, disabled model visibility, and catalog-facing ids. | | OAuth | Login/status/logout for OAuth-backed providers, plus multiauth account management: `GET /api/oauth/accounts`, `PUT /api/oauth/accounts/active`, `PUT /api/oauth/accounts/alias`, `DELETE /api/oauth/accounts` list masked accounts per provider, switch the active one, edit its display-only alias, and remove one. Login accepts `addAccount: true` to force a fresh browser identity. Device flows return a structured `deviceCode`; the GUI highlights and copies it before the user opens the verification page. | @@ -44,6 +45,14 @@ The dashboard sidebar includes a stop button that calls `POST /api/stop`. The bu confirmation prompt, then fires the request and accepts the connection drop (the proxy exits). The endpoint restores native Codex config, stops any installed service to prevent respawn, and exits. +## Startup safety + +The dashboard sidebar exposes a **Startup safety** page. Its warning state is derived from active +Codex routing plus the actual service and launcher-shim installation state; the +`codexAutoStart` preference alone is never presented as proof of restart protection. The page is +read-only and shows copyable repair commands (`ocx service install`, `ocx codex-shim install`, and +`ocx restore`) instead of installing lifecycle hooks from the proxy process. + ## UX boundary The dashboard is a local control surface, not a separate service. It should reflect the same config diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts new file mode 100644 index 00000000000..9246410a388 --- /dev/null +++ b/tests/autostart-health.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test } from "bun:test"; +import { deriveStartupHealth, startupHealthSummary } from "../src/codex/autostart-health"; +import { hasInjectedCodexRouting } from "../src/codex/inject"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { OcxConfig } from "../src/types"; + +const base = { + routingInjected: true, + autostartEnabled: true, + serviceInstalled: false, + serviceSupported: true, + shimInstalled: false, + shimHealthy: false, + platform: "win32" as const, +}; + +describe("Codex startup health", () => { + test("flags injected routing without a persistent starter as restart-unsafe", () => { + const health = deriveStartupHealth(base); + expect(health).toMatchObject({ + status: "at-risk", + rebootSafe: false, + protection: "none", + recommendedCommand: "ocx service install", + }); + expect(startupHealthSummary(health)).toContain("AT RISK"); + }); + + test("treats a background service as restart protection", () => { + const health = deriveStartupHealth({ ...base, serviceInstalled: true }); + expect(health).toMatchObject({ + status: "protected", + rebootSafe: true, + protection: "service", + recommendedCommand: null, + }); + }); + + test("classifies a healthy Windows shim as CLI-only rather than Desktop-safe", () => { + const windowsShim = deriveStartupHealth({ ...base, shimInstalled: true, shimHealthy: true }); + expect(windowsShim).toMatchObject({ protection: "shim", shimCoverage: "cli-only", status: "at-risk" }); + const unixShim = deriveStartupHealth({ ...base, platform: "linux", shimInstalled: true, shimHealthy: true }); + expect(unixShim).toMatchObject({ protection: "shim", shimCoverage: "full", status: "protected" }); + expect(deriveStartupHealth({ ...base, shimInstalled: true, shimHealthy: false }).status).toBe("at-risk"); + expect(deriveStartupHealth({ ...base, autostartEnabled: false, shimInstalled: true, shimHealthy: true }).status).toBe("at-risk"); + }); + + test("native routing has no opencodex restart dependency", () => { + const health = deriveStartupHealth({ ...base, routingInjected: false }); + expect(health).toMatchObject({ status: "native", rebootSafe: true, protection: "none" }); + }); + + test("recognizes marker-owned and legacy routing without claiming user overrides", () => { + expect(hasInjectedCodexRouting([ + '# Auto-injected by opencodex', + 'openai_base_url = "http://127.0.0.1:10100/v1"', + "[features]", + ].join("\n"))).toBe(true); + expect(hasInjectedCodexRouting([ + 'model_provider = "opencodex"', + "[model_providers.opencodex]", + 'base_url = "http://127.0.0.1:10100/v1"', + ].join("\n"))).toBe(true); + expect(hasInjectedCodexRouting('openai_base_url = "http://127.0.0.1:10100/v1"')).toBe(false); + }); + + test("exposes a secret-free startup health DTO to the dashboard", async () => { + const url = new URL("http://localhost/api/startup-health"); + const response = await handleManagementAPI( + new Request(url), + url, + { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, + ); + expect(response?.status).toBe(200); + + const body = await response!.json() as Record; + expect(["native", "protected", "at-risk"]).toContain(body.status); + expect(typeof body.rebootSafe).toBe("boolean"); + expect(typeof body.routingInjected).toBe("boolean"); + expect(body.commands).toEqual({ + installService: "ocx service install", + installShim: "ocx codex-shim install", + restoreNative: "ocx restore", + }); + + const serialized = JSON.stringify(body).toLowerCase(); + for (const secretName of ["api_key", "apikey", "authorization", "access_token", "refresh_token"]) { + expect(serialized).not.toContain(secretName); + } + }); +}); diff --git a/tests/cli-status-json.test.ts b/tests/cli-status-json.test.ts index 42f60752334..9c8e7db52dd 100644 --- a/tests/cli-status-json.test.ts +++ b/tests/cli-status-json.test.ts @@ -53,6 +53,17 @@ describe("CLI status JSON", () => { paths?: { config?: unknown; pid?: unknown; runtime?: unknown }; runtime?: { source?: unknown }; codexAutostart?: unknown; + startup?: { + status?: unknown; + rebootSafe?: unknown; + routingInjected?: unknown; + serviceInstalled?: unknown; + shimInstalled?: unknown; + shimHealthy?: unknown; + shimCoverage?: unknown; + serviceSupported?: unknown; + commands?: unknown; + }; defaultProvider?: unknown; config?: { source?: unknown; error?: unknown }; service?: { summary?: unknown }; @@ -73,6 +84,15 @@ describe("CLI status JSON", () => { expect(typeof parsed.paths?.runtime).toBe("string"); expect(typeof parsed.runtime?.source).toBe("string"); expect(parsed.codexAutostart).toBe(false); + expect(["native", "protected", "at-risk"]).toContain(parsed.startup?.status); + expect(typeof parsed.startup?.rebootSafe).toBe("boolean"); + expect(typeof parsed.startup?.routingInjected).toBe("boolean"); + expect(typeof parsed.startup?.serviceInstalled).toBe("boolean"); + expect(typeof parsed.startup?.shimInstalled).toBe("boolean"); + expect(typeof parsed.startup?.shimHealthy).toBe("boolean"); + expect(["full", "cli-only", "none"]).toContain(parsed.startup?.shimCoverage); + expect(typeof parsed.startup?.serviceSupported).toBe("boolean"); + expect(typeof parsed.startup?.commands).toBe("object"); expect(parsed.defaultProvider).toBe("openai"); expect(parsed.config?.source).toBe("file"); expect(parsed.config?.error).toBeNull(); diff --git a/tests/codex-shim.test.ts b/tests/codex-shim.test.ts index c711b16b37f..d7176f5a027 100644 --- a/tests/codex-shim.test.ts +++ b/tests/codex-shim.test.ts @@ -3,7 +3,7 @@ import { spawnSync } from "node:child_process"; import { chmodSync, mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, findCodexOnPath, installCodexShim, isWindowsInteropDir, lastCodexDiscoveryError, uninstallCodexShim } from "../src/codex/shim"; +import { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, installCodexShim, isWindowsInteropDir, lastCodexDiscoveryError, uninstallCodexShim } from "../src/codex/shim"; const SHIM_MARKER = "opencodex codex autostart shim"; @@ -301,10 +301,12 @@ describe("Codex autostart shim", () => { const state = JSON.parse(readFileSync(join(home, "codex-shim.json"), "utf8")); expect(state.wrappers).toHaveLength(3); + expect(diagnoseCodexShim()).toMatchObject({ installed: true, healthy: true }); const removed = uninstallCodexShim(); expect(removed.removed).toBe(true); + expect(diagnoseCodexShim()).toMatchObject({ installed: false, healthy: false }); expect(readFileSync(cmd, "utf8")).toBe(cmdOriginal); expect(readFileSync(ps1, "utf8")).toBe(ps1Original); expect(readFileSync(bare, "utf8")).toBe(bareOriginal); From c1ecbe1b54df024170f6f13063083aa1d0004569 Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:07:57 +0900 Subject: [PATCH 02/11] feat(windows): add restart-safe tray controls --- bin/ocx.mjs | 43 ++ .../src/content/docs/guides/web-dashboard.md | 2 + .../content/docs/ja/guides/web-dashboard.md | 2 + .../content/docs/ko/guides/web-dashboard.md | 2 + .../content/docs/ru/guides/web-dashboard.md | 2 + .../docs/zh-cn/guides/web-dashboard.md | 2 + gui/src/App.tsx | 23 +- gui/src/i18n/de.ts | 23 + gui/src/i18n/en.ts | 23 + gui/src/i18n/ja.ts | 23 + gui/src/i18n/ko.ts | 23 + gui/src/i18n/ru.ts | 23 + gui/src/i18n/zh.ts | 23 + gui/src/pages/Dashboard.tsx | 8 +- gui/src/pages/Startup.tsx | 176 +++++- gui/src/styles.css | 2 + src/cli/doctor.ts | 2 +- src/cli/help.ts | 9 + src/cli/index.ts | 75 ++- src/cli/status.ts | 18 +- src/codex/autostart-health.ts | 75 ++- src/codex/inject.ts | 71 ++- src/codex/shim.ts | 40 +- src/server/management/config-routes.ts | 34 +- src/server/startup-health-cache.ts | 98 ++++ src/server/windows-tray-control.ts | 41 ++ src/service.ts | 157 +++++- src/tray/windows-tray.ps1 | 273 ++++++++++ src/tray/windows.ts | 507 ++++++++++++++++++ src/update/index.ts | 45 ++ src/update/job.ts | 42 ++ structure/05_gui-and-management-api.md | 10 +- tests/autostart-health.test.ts | 77 ++- tests/service.test.ts | 46 +- tests/windows-tray.test.ts | 143 +++++ 35 files changed, 2072 insertions(+), 91 deletions(-) create mode 100644 src/server/startup-health-cache.ts create mode 100644 src/server/windows-tray-control.ts create mode 100644 src/tray/windows-tray.ps1 create mode 100644 src/tray/windows.ts create mode 100644 tests/windows-tray.test.ts diff --git a/bin/ocx.mjs b/bin/ocx.mjs index 06a7a01380a..edf794752d1 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -90,6 +90,27 @@ function repairCodexShimIfNeeded() { } } +function trayInstallState() { + const statePath = join(configDir(), "tray-state.json"); + if (!existsSync(statePath)) return { installed: false, running: false }; + let running = false; + try { + const heartbeat = JSON.parse(readFileSync(join(configDir(), "tray-heartbeat.json"), "utf8")); + if (Number.isSafeInteger(heartbeat.pid) && Date.now() - Number(heartbeat.timestamp) < 15_000) { + process.kill(heartbeat.pid, 0); + running = true; + } + } catch { /* installed but not running */ } + return { installed: true, running }; +} + +function runTrayLifecycle(launcher, action) { + return spawnSync(process.execPath, [launcher, "tray", action], { + stdio: "inherit", + windowsHide: true, + }); +} + function runNpmSelfUpdate() { const current = currentPackageVersion(); const tag = updateTag(current); @@ -115,6 +136,7 @@ function runNpmSelfUpdate() { // unloads it permanently, so a successful update must reinstall it afterwards. const serviceStatePath = join(configDir(), "service-state.json"); const serviceWasInstalled = existsSync(serviceStatePath); + const trayBeforeUpdate = process.platform === "win32" ? trayInstallState() : { installed: false, running: false }; /** Read the backend from service-state.json so the update reinstalls the same one. */ function serviceReinstallArgs() { try { @@ -160,6 +182,15 @@ function runNpmSelfUpdate() { // and the runtime-port record too: a service-managed or orphaned proxy can be live // while ocx.pid is stale/missing. const launcher = fileURLToPath(import.meta.url); + if (trayBeforeUpdate.running) { + console.log("⏹ Handing off the Windows tray before updating..."); + const stopped = runTrayLifecycle(launcher, "stop"); + if (stopped.status !== 0 || trayInstallState().running) { + runTrayLifecycle(launcher, "start"); + console.error("opencodex: could not stop the Windows tray; aborting before package replacement."); + process.exit(1); + } + } const hasRuntimeState = existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json")); if (serviceWasInstalled || hasRuntimeState) { @@ -168,6 +199,7 @@ function runNpmSelfUpdate() { const stillHasRuntimeState = existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json")); if (stopRes.status !== 0 || stillHasRuntimeState) { + if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); console.error("opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry."); process.exit(1); } @@ -190,6 +222,16 @@ function runNpmSelfUpdate() { if (res.status === 0) { console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`); repairCodexShimIfNeeded(); + if (trayBeforeUpdate.installed) { + const tray = spawnSync(process.execPath, [launcher, "tray", "install", ...(!trayBeforeUpdate.running ? ["--no-start"] : [])], { + stdio: "inherit", + windowsHide: true, + }); + if (tray.status !== 0) { + console.warn("opencodex: Windows tray refresh failed. Run: ocx tray install"); + if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); + } + } // The stop above unloaded any managed service; reinstall via the freshly-installed // launcher so the new files write the baked paths and the service restarts. if (serviceWasInstalled) { @@ -226,6 +268,7 @@ function runNpmSelfUpdate() { } process.exit(0); } + if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); console.error(`\nUpdate failed (${npm} exit ${res.status ?? "?"}). Try manually: ${npm} install -g ${PKG}@${tag}`); process.exit(1); } diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index d5d0f1c5347..f26f9bc122d 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -30,6 +30,7 @@ bun run dev:gui | **Sidecars** | Choose the web-search model and effort plus the vision-description model. Changes apply on the next request. | | **Maintenance** | Resync the Codex model catalog, inspect project-local config bypass warnings, check the latest or preview release, and run an update with optional proxy restart. | | **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. | +| **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. | | **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. | | **Providers** | Add, edit, enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. | | **Add provider** | Search registry-backed presets for account login, API-key services, local servers, or a custom endpoint. | @@ -88,6 +89,7 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints | --- | --- | | `GET` / `PUT /api/settings` | Read settings or toggle Codex autostart. | | `GET /api/startup-health` | Read secret-free routing, service, shim, and restart-safety diagnostics. | +| `GET` / `POST /api/windows-tray` | Read or change the Windows tray installation and visible-process state. POST accepts `install`, `start`, `stop`, or `uninstall`. | | `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. | | `GET` / `PUT /api/sidecar-settings` | Read or set search/vision sidecar model settings. | diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md index 2505e258ad2..3cb6bd4dd47 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -30,6 +30,7 @@ bun run dev:gui | **サイドカー** | ウェブ検索モデルと強度、画像説明モデルを選択します。次回リクエストから適用されます。 | | **メンテナンス** | Codex モデルカタログを再同期し、プロジェクトローカル設定のバイパス警告を確認し、latest/preview 更新を照会またはオプションのプロキシ再起動と共にインストールします。 | | **起動安全性** | 注入された Codex ルーティングが再起動後も機能するか、サービスと launcher shim の状態、正確な修復コマンドと共に表示します。 | +| **Windows トレイ** | ユーザーのログイントレイを導入し、プロキシ開始・停止・再起動・ダッシュボード・状態をクリックで操作します。トレイは再起動サービスではありません。 | | **Codex 自動起動** | インストール済み Codex launcher shim に `ocx ensure` の実行を許可します。このトグルは shim やバックグラウンドサービスをインストールしません。 | | **プロバイダー** | プロバイダーを追加、編集、有効化/無効化、削除し、対応する OAuth アカウントプールと API キープールを管理します。 | | **プロバイダー追加** | レジストリベースのプリセットからアカウントログイン、API キーサービス、ローカルサーバー、custom エンドポイントを検索します。 | @@ -83,6 +84,7 @@ GUI はプロキシの JSON 管理 API を使うシンクライアントです --- | --- | | `GET` / `PUT /api/settings` | 設定を読むか Codex 自動起動をオン/オフします。 | | `GET /api/startup-health` | 秘密情報を含まないルーティング、サービス、shim、再起動安全性診断を読み取ります。 | +| `GET` / `POST /api/windows-tray` | Windows トレイの導入・表示状態を読み取り、`install`、`start`、`stop`、`uninstall` を実行します。 | | `POST /api/sync` | 共有モデルカタログを再構築し Codex モデルキャッシュを古い状態としてマークします。 | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 自己更新作業を確認、実行、追跡します。 | | `GET` / `PUT /api/sidecar-settings` | 検索/ビジョンサイドカーモデル設定を読むか変えます。 | diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index 0a8d68f07b5..21a0c4c57ca 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -30,6 +30,7 @@ bun run dev:gui | **사이드카** | 웹 검색 모델과 강도, 이미지 설명 모델을 선택합니다. 다음 요청부터 적용됩니다. | | **Maintenance** | Codex 모델 카탈로그를 다시 동기화하고, 프로젝트 로컬 설정의 우회 경고를 확인하고, latest/preview 업데이트를 조회하거나 선택적 프록시 재시작과 함께 설치합니다. | | **시작 안전성** | 주입된 Codex 라우팅이 재부팅 후에도 유지되는지 서비스와 launcher shim 상태, 정확한 복구 명령과 함께 표시합니다. | +| **Windows 트레이** | 로그인할 때 사용자 전용 트레이를 시작하고 프록시 시작·중지·재시작·대시보드·상태를 클릭으로 제어합니다. 트레이는 재시작 서비스가 아닙니다. | | **Codex 자동 시작** | 이미 설치된 Codex launcher shim이 `ocx ensure`를 실행하도록 허용합니다. 이 토글은 shim이나 백그라운드 서비스를 설치하지 않습니다. | | **Providers** | 프로바이더를 추가, 편집, 활성화/비활성화, 제거하고, 지원되는 OAuth 계정 풀과 API key 풀을 관리합니다. | | **Add provider** | 레지스트리 기반 프리셋에서 계정 로그인, API key 서비스, 로컬 서버, custom endpoint를 검색합니다. | @@ -85,6 +86,7 @@ GUI는 프록시의 JSON 관리 API를 사용하는 얇은 클라이언트입니 | --- | --- | | `GET` / `PUT /api/settings` | 설정을 읽거나 Codex 자동 시작을 켜고 끕니다. | | `GET /api/startup-health` | 비밀값 없이 라우팅, 서비스, shim, 재부팅 안전성 진단을 읽습니다. | +| `GET` / `POST /api/windows-tray` | Windows 트레이 설치 및 표시 상태를 읽거나 `install`, `start`, `stop`, `uninstall` 작업을 수행합니다. | | `POST /api/sync` | 공유 모델 카탈로그를 다시 만들고 Codex 모델 캐시를 오래된 상태로 표시합니다. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 자체 업데이트 작업을 확인, 실행, 추적합니다. | | `GET` / `PUT /api/sidecar-settings` | 검색/비전 사이드카 모델 설정을 읽거나 바꿉니다. | diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index def6845a17e..2c831e87ec9 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -30,6 +30,7 @@ bun run dev:gui | **Сайдкары** | Выбор модели и уровня рассуждений для веб-поиска, а также модели описания изображений. Изменения применяются со следующего запроса. | | **Maintenance** | Пересинхронизация каталога моделей Codex, просмотр предупреждений об обходе через проектную локальную конфигурацию, проверка последнего или предварительного выпуска и запуск обновления с необязательным перезапуском прокси. | | **Безопасность запуска** | Показывает, сохранит ли внедрённая маршрутизация Codex работоспособность после перезагрузки, отдельно отображая службу, launcher shim и точные команды исправления. | +| **Трей Windows** | Устанавливает пользовательский значок входа для запуска, остановки, перезапуска, панели и состояния прокси одним щелчком. Трей не является службой перезапуска. | | **Автозапуск Codex** | Разрешает уже установленному launcher shim Codex выполнять `ocx ensure`. Переключатель не устанавливает shim или фоновую службу. | | **Providers** | Добавление, редактирование, включение/отключение и удаление провайдеров; управление пулами OAuth-аккаунтов и пулами API-ключей там, где они поддерживаются. | | **Add provider** | Поиск по пресетам из реестра: вход по аккаунту, сервисы с API-ключом, локальные серверы или пользовательская конечная точка. | @@ -88,6 +89,7 @@ GUI — это тонкий клиент поверх JSON-API управлен | --- | --- | | `GET` / `PUT /api/settings` | Чтение настроек или переключение автозапуска Codex. | | `GET /api/startup-health` | Чтение безопасной диагностики маршрутизации, службы, shim и устойчивости к перезагрузке. | +| `GET` / `POST /api/windows-tray` | Чтение или изменение установки и видимости трея Windows; POST поддерживает `install`, `start`, `stop`, `uninstall`. | | `POST /api/sync` | Пересборка общего каталога моделей и инвалидация кэша моделей Codex. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Проверка, запуск и мониторинг задач самообновления. | | `GET` / `PUT /api/sidecar-settings` | Чтение или настройка моделей сайдкаров поиска/vision. | diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 08fd0b6df68..2d910dc118f 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -29,6 +29,7 @@ bun run dev:gui | **Sidecar** | 选择 web-search 模型及强度,以及图像描述模型;更改从下一次请求开始生效。 | | **Maintenance** | 重新同步 Codex 模型目录,查看项目级配置绕过警告,检查 latest/preview 版本,并可在更新后重启代理。 | | **启动安全** | 显示注入的 Codex 路由能否在重启后继续工作,并分别显示服务、launcher shim 状态和准确的修复命令。 | +| **Windows 托盘** | 安装用户登录托盘,一键控制代理启动、停止、重启、面板和状态。托盘不是代理重启服务。 | | **Codex 自动启动** | 允许已安装的 Codex launcher shim 运行 `ocx ensure`。此开关不会安装 shim 或后台服务。 | | **Providers** | 添加、编辑、启用/禁用、删除 provider,并在支持时管理 OAuth 账号池和 API key 池。 | | **Add provider** | 搜索 registry preset,选择账号登录、API key 服务、本地服务器或自定义 endpoint。 | @@ -80,6 +81,7 @@ GUI 是代理 JSON 管理 API 之上的轻量客户端。常用 endpoint 包括 | --- | --- | | `GET` / `PUT /api/settings` | 读取设置或切换 Codex 自动启动。 | | `GET /api/startup-health` | 读取不含秘密信息的路由、服务、shim 和重启安全诊断。 | +| `GET` / `POST /api/windows-tray` | 读取或更改 Windows 托盘安装和显示状态;POST 支持 `install`、`start`、`stop`、`uninstall`。 | | `POST /api/sync` | 重建共享模型目录,并把 Codex 模型缓存标记为过期。 | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | 检查、运行和监控自更新任务。 | | `GET` / `PUT /api/sidecar-settings` | 读取或设置 search/vision sidecar 模型。 | diff --git a/gui/src/App.tsx b/gui/src/App.tsx index 19d0a63b9ce..f5b327cc1a0 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -140,18 +140,31 @@ export default function App() { useEffect(() => { let cancelled = false; + let latestRequest = 0; const readStartupHealth = async () => { + const request = ++latestRequest; try { const res = await fetch(`${API_BASE}/api/startup-health`); - if (!res.ok) return; - const data = await res.json() as { status?: unknown }; - if (!cancelled) setStartupAtRisk(data.status === "at-risk"); + if (!res.ok) { + if (!cancelled && request === latestRequest) setStartupAtRisk(true); + return; + } + const data = await res.json() as { status?: unknown; diagnosticStale?: unknown }; + const valid = data.status === "native" || data.status === "protected" || data.status === "at-risk"; + if (!cancelled && request === latestRequest) { + setStartupAtRisk(!valid || data.status === "at-risk" || data.diagnosticStale === true); + } } catch { - // The proxy may be stopping; keep the last known lifecycle state. + // A failed refresh cannot preserve a previously green claim. + if (!cancelled && request === latestRequest) setStartupAtRisk(true); } }; void readStartupHealth(); - return () => { cancelled = true; }; + const timer = window.setInterval(() => { void readStartupHealth(); }, 30_000); + return () => { + cancelled = true; + window.clearInterval(timer); + }; }, []); useEffect(() => { diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 01132da1fd2..4805d490f42 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -25,6 +25,7 @@ export const de = { "startup.refresh": "Aktualisieren", "startup.loading": "Startschutz wird geprüft…", "startup.error": "Startschutz konnte nicht gelesen werden.", + "startup.staleData": "Die aktuelle Prüfung ist fehlgeschlagen. Die Werte unten sind veraltet und kein Nachweis für Schutz.", "startup.status.native": "Natives Routing", "startup.status.protected": "Neustartgeschützt", "startup.status.atRisk": "Aktion erforderlich", @@ -32,11 +33,14 @@ export const de = { "startup.summary.protected": "opencodex ist nach einem Neustart verfügbar", "startup.summary.atRisk": "Codex kann nach einem Neustart den Modellzugriff verlieren", "startup.riskDetail": "Codex ist auf den lokalen Proxy festgelegt, aber weder ein dauerhafter Dienst noch ein intakter Launcher-Shim startet ihn erneut.", + "startup.riskDetailCustomLocal": "Codex verwendet ein benutzerdefiniertes lokales Gateway. opencodex kann dessen Neustart-Lebenszyklus weder verwalten noch prüfen.", "startup.riskDetailWindowsShim": "Der Launcher-Shim schützt unterstützte CLI-Skripte, aber Codex Desktop und direkte codex.exe-Aufrufe können ihn unter Windows umgehen.", "startup.safeDetail": "Routing und Startmechanismus stimmen überein. Nach einem Neustart sollte kein manuelles ocx start nötig sein.", "startup.routing": "Codex-Routing", "startup.routing.proxy": "Lokaler Proxy", "startup.routing.native": "Natives OpenAI", + "startup.routing.customLocal": "Benutzerdefiniertes lokales Gateway", + "startup.routing.customRemote": "Benutzerdefiniertes Remote-Gateway", "startup.restartProtection": "Neustartschutz", "startup.preference": "Start bei Bedarf", "startup.enabled": "Aktiviert", @@ -55,6 +59,25 @@ export const de = { "startup.healthy": "Intakt", "startup.cliOnly": "Nur CLI", "startup.stale": "Veraltet", + "startup.viable": "Einsatzbereit", + "startup.unhealthy": "Installiert, aber fehlerhaft", + "startup.conflict": "Dienstkonflikt", + "startup.installedDisabled": "Installiert, aber deaktiviert", + "startup.tray.title": "Windows-Infobereich", + "startup.tray.hint": "Installiert ein Anmeldesymbol für Proxy-Start, Stopp, Neustart, Dashboard und Status per Klick.", + "startup.tray.login": "Infobereich bei Windows-Anmeldung starten", + "startup.tray.notProtection": "Das Symbol ist nur eine Steuerung, kein Neustartschutz. Für unbeaufsichtigte Wiederherstellung bleibt ein funktionsfähiger Hintergrunddienst nötig.", + "startup.tray.running": "Wird ausgeführt", + "startup.tray.stopped": "Installiert, ausgeblendet", + "startup.tray.stale": "Reparatur erforderlich", + "startup.tray.notInstalled": "Nicht installiert", + "startup.tray.loading": "Wird geprüft…", + "startup.tray.unavailable": "Status nicht verfügbar", + "startup.tray.install": "Installieren und anzeigen", + "startup.tray.start": "Symbol anzeigen", + "startup.tray.stop": "Symbol beenden", + "startup.tray.uninstall": "Anmeldesymbol entfernen", + "startup.tray.error": "Die Windows-Infobereichsaktion ist fehlgeschlagen. Details: ocx tray status.", "startup.recovery": "Reparaturoptionen", "startup.recoveryHint": "Führe einen Befehl im Terminal aus. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.", "startup.command.service": "Empfohlen: dauerhafter Hintergrunddienst", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 84f5a2c65d1..55c7700c945 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -34,6 +34,7 @@ export const en = { "startup.refresh": "Refresh", "startup.loading": "Checking startup protection…", "startup.error": "Could not read startup protection.", + "startup.staleData": "The latest startup check failed. The values below are stale and must not be treated as proof of protection.", "startup.status.native": "Native routing", "startup.status.protected": "Restart protected", "startup.status.atRisk": "Action required", @@ -41,11 +42,14 @@ export const en = { "startup.summary.protected": "opencodex will be available after restart", "startup.summary.atRisk": "Codex can lose model access after restart", "startup.riskDetail": "Codex is pinned to the local proxy, but no persistent service or healthy launcher shim will start it again.", + "startup.riskDetailCustomLocal": "Codex points to a custom local gateway. opencodex cannot manage or verify that gateway's restart lifecycle.", "startup.riskDetailWindowsShim": "The launcher shim protects supported CLI scripts, but Codex Desktop and direct codex.exe launches can bypass it on Windows.", "startup.safeDetail": "The current routing and startup mechanism are consistent. No manual ocx start should be required after restart.", "startup.routing": "Codex routing", "startup.routing.proxy": "Local proxy", "startup.routing.native": "Native OpenAI", + "startup.routing.customLocal": "Custom local gateway", + "startup.routing.customRemote": "Custom remote gateway", "startup.restartProtection": "Restart protection", "startup.preference": "On-demand startup", "startup.enabled": "Enabled", @@ -64,6 +68,25 @@ export const en = { "startup.healthy": "Healthy", "startup.cliOnly": "CLI only", "startup.stale": "Stale", + "startup.viable": "Ready", + "startup.unhealthy": "Installed but unhealthy", + "startup.conflict": "Service conflict", + "startup.installedDisabled": "Installed but disabled", + "startup.tray.title": "Windows system tray", + "startup.tray.hint": "Install a login tray icon for one-click proxy start, stop, restart, dashboard, and status controls.", + "startup.tray.login": "Start tray at Windows login", + "startup.tray.notProtection": "The tray is a controller, not restart protection. A viable background service is still required for unattended proxy recovery.", + "startup.tray.running": "Running", + "startup.tray.stopped": "Installed, hidden", + "startup.tray.stale": "Repair required", + "startup.tray.notInstalled": "Not installed", + "startup.tray.loading": "Checking…", + "startup.tray.unavailable": "Status unavailable", + "startup.tray.install": "Install and show tray", + "startup.tray.start": "Show tray icon", + "startup.tray.stop": "Exit tray icon", + "startup.tray.uninstall": "Remove login tray", + "startup.tray.error": "The Windows tray action failed. Check ocx tray status for details.", "startup.recovery": "Repair options", "startup.recoveryHint": "Run one command in a terminal. The background service is recommended for Codex Desktop and Windows executables.", "startup.command.service": "Recommended: persistent background service", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 40cfca20ab4..538cec86561 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -34,6 +34,7 @@ export const ja: Record = { "startup.refresh": "更新", "startup.loading": "起動保護を確認中…", "startup.error": "起動保護を読み取れませんでした。", + "startup.staleData": "最新の確認に失敗しました。以下は古い値であり、保護の証明にはなりません。", "startup.status.native": "ネイティブルーティング", "startup.status.protected": "再起動保護済み", "startup.status.atRisk": "対応が必要", @@ -41,11 +42,14 @@ export const ja: Record = { "startup.summary.protected": "再起動後も opencodex を利用できます", "startup.summary.atRisk": "再起動後に Codex がモデルへ接続できなくなる可能性があります", "startup.riskDetail": "Codex はローカルプロキシを参照していますが、再起動する永続サービスまたは正常な launcher shim がありません。", + "startup.riskDetailCustomLocal": "Codex はカスタムローカルゲートウェイを参照しています。opencodex はその再起動ライフサイクルを管理・検証できません。", "startup.riskDetailWindowsShim": "Launcher shim は対応する CLI スクリプトのみを保護し、Windows の Codex Desktop と codex.exe の直接起動はこれを迂回できます。", "startup.safeDetail": "現在のルーティングと起動方式は整合しています。再起動後に ocx start を手動実行する必要はありません。", "startup.routing": "Codex ルーティング", "startup.routing.proxy": "ローカルプロキシ", "startup.routing.native": "OpenAI ネイティブ", + "startup.routing.customLocal": "カスタムローカルゲートウェイ", + "startup.routing.customRemote": "カスタム遠隔ゲートウェイ", "startup.restartProtection": "再起動保護", "startup.preference": "オンデマンド起動", "startup.enabled": "有効", @@ -64,6 +68,25 @@ export const ja: Record = { "startup.healthy": "正常", "startup.cliOnly": "CLI のみ", "startup.stale": "破損", + "startup.viable": "利用可能", + "startup.unhealthy": "インストール済み・異常", + "startup.conflict": "サービス競合", + "startup.installedDisabled": "インストール済み・無効", + "startup.tray.title": "Windows システムトレイ", + "startup.tray.hint": "ログイン時にトレイを起動し、プロキシの開始・停止・再起動・ダッシュボード・状態をクリックで操作します。", + "startup.tray.login": "Windows ログイン時にトレイを開始", + "startup.tray.notProtection": "トレイは操作画面であり再起動保護ではありません。無人復旧には正常なバックグラウンドサービスが必要です。", + "startup.tray.running": "実行中", + "startup.tray.stopped": "インストール済み・非表示", + "startup.tray.stale": "修復が必要", + "startup.tray.notInstalled": "未インストール", + "startup.tray.loading": "確認中…", + "startup.tray.unavailable": "状態を確認できません", + "startup.tray.install": "トレイをインストールして表示", + "startup.tray.start": "トレイアイコンを表示", + "startup.tray.stop": "トレイアイコンを終了", + "startup.tray.uninstall": "ログイントレイを削除", + "startup.tray.error": "Windows トレイ操作に失敗しました。ocx tray status で詳細を確認してください。", "startup.recovery": "修復方法", "startup.recoveryHint": "ターミナルでコマンドを1つ実行します。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。", "startup.command.service": "推奨: 永続バックグラウンドサービス", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 8bd8b2c7d6a..42c8958436a 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -29,6 +29,7 @@ export const ko: Record = { "startup.refresh": "새로고침", "startup.loading": "시작 보호 상태 확인 중…", "startup.error": "시작 보호 상태를 읽지 못했습니다.", + "startup.staleData": "최신 시작 상태 확인에 실패했습니다. 아래 값은 이전 결과이며 보호 증거로 사용하면 안 됩니다.", "startup.status.native": "네이티브 라우팅", "startup.status.protected": "재부팅 보호됨", "startup.status.atRisk": "조치 필요", @@ -36,11 +37,14 @@ export const ko: Record = { "startup.summary.protected": "재부팅 후에도 opencodex가 자동으로 준비됩니다", "startup.summary.atRisk": "재부팅 후 Codex 모델 연결이 끊길 수 있습니다", "startup.riskDetail": "Codex는 로컬 프록시를 바라보지만 이를 다시 시작할 영구 서비스나 정상 launcher shim이 없습니다.", + "startup.riskDetailCustomLocal": "Codex가 사용자 지정 로컬 게이트웨이를 바라봅니다. opencodex는 해당 게이트웨이의 재시작 수명주기를 관리하거나 검증할 수 없습니다.", "startup.riskDetailWindowsShim": "Launcher shim은 지원되는 CLI 스크립트만 보호하며 Windows의 Codex Desktop과 직접 codex.exe 실행은 이를 우회할 수 있습니다.", "startup.safeDetail": "현재 라우팅과 시작 방식이 일치합니다. 재부팅 후 ocx start를 수동으로 실행할 필요가 없습니다.", "startup.routing": "Codex 라우팅", "startup.routing.proxy": "로컬 프록시", "startup.routing.native": "OpenAI 네이티브", + "startup.routing.customLocal": "사용자 지정 로컬 게이트웨이", + "startup.routing.customRemote": "사용자 지정 원격 게이트웨이", "startup.restartProtection": "재부팅 보호", "startup.preference": "필요 시 자동 시작", "startup.enabled": "켜짐", @@ -59,6 +63,25 @@ export const ko: Record = { "startup.healthy": "정상", "startup.cliOnly": "CLI 전용", "startup.stale": "손상됨", + "startup.viable": "사용 가능", + "startup.unhealthy": "설치됐지만 비정상", + "startup.conflict": "서비스 충돌", + "startup.installedDisabled": "설치됐지만 꺼짐", + "startup.tray.title": "Windows 시스템 트레이", + "startup.tray.hint": "로그인할 때 트레이 아이콘을 띄우고 프록시 시작·중지·재시작·대시보드·상태를 클릭으로 제어합니다.", + "startup.tray.login": "Windows 로그인 시 트레이 시작", + "startup.tray.notProtection": "트레이는 제어 화면이며 재부팅 보호 서비스가 아닙니다. 무인 복구에는 정상 백그라운드 서비스가 별도로 필요합니다.", + "startup.tray.running": "실행 중", + "startup.tray.stopped": "설치됨, 숨김", + "startup.tray.stale": "복구 필요", + "startup.tray.notInstalled": "설치되지 않음", + "startup.tray.loading": "확인 중…", + "startup.tray.unavailable": "상태 확인 불가", + "startup.tray.install": "트레이 설치 및 표시", + "startup.tray.start": "트레이 아이콘 표시", + "startup.tray.stop": "트레이 아이콘 종료", + "startup.tray.uninstall": "로그인 트레이 제거", + "startup.tray.error": "Windows 트레이 작업에 실패했습니다. ocx tray status에서 상세 내용을 확인하세요.", "startup.recovery": "복구 방법", "startup.recoveryHint": "터미널에서 명령 하나를 실행하세요. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.", "startup.command.service": "권장: 영구 백그라운드 서비스", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 11aa35826f6..45b9939f7c6 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -34,6 +34,7 @@ export const ru: Record = { "startup.refresh": "Обновить", "startup.loading": "Проверка защиты запуска…", "startup.error": "Не удалось прочитать состояние защиты запуска.", + "startup.staleData": "Последняя проверка не удалась. Значения ниже устарели и не подтверждают защиту.", "startup.status.native": "Нативная маршрутизация", "startup.status.protected": "Перезапуск защищён", "startup.status.atRisk": "Требуется действие", @@ -41,11 +42,14 @@ export const ru: Record = { "startup.summary.protected": "opencodex будет доступен после перезагрузки", "startup.summary.atRisk": "После перезагрузки Codex может потерять доступ к моделям", "startup.riskDetail": "Codex направлен на локальный прокси, но постоянная служба или исправный launcher shim не запустят его снова.", + "startup.riskDetailCustomLocal": "Codex направлен на пользовательский локальный шлюз. opencodex не может управлять или проверять его перезапуск.", "startup.riskDetailWindowsShim": "Launcher shim защищает поддерживаемые CLI-скрипты, но Codex Desktop и прямой запуск codex.exe в Windows могут обходить его.", "startup.safeDetail": "Маршрутизация и механизм запуска согласованы. После перезагрузки ручной запуск ocx start не требуется.", "startup.routing": "Маршрутизация Codex", "startup.routing.proxy": "Локальный прокси", "startup.routing.native": "Нативный OpenAI", + "startup.routing.customLocal": "Пользовательский локальный шлюз", + "startup.routing.customRemote": "Пользовательский удалённый шлюз", "startup.restartProtection": "Защита перезапуска", "startup.preference": "Запуск по требованию", "startup.enabled": "Включён", @@ -64,6 +68,25 @@ export const ru: Record = { "startup.healthy": "Исправен", "startup.cliOnly": "Только CLI", "startup.stale": "Повреждён", + "startup.viable": "Готов", + "startup.unhealthy": "Установлен, но неисправен", + "startup.conflict": "Конфликт служб", + "startup.installedDisabled": "Установлен, но отключён", + "startup.tray.title": "Системный трей Windows", + "startup.tray.hint": "Запускает значок при входе для управления запуском, остановкой, перезапуском, панелью и состоянием прокси.", + "startup.tray.login": "Запускать трей при входе в Windows", + "startup.tray.notProtection": "Трей — это контроллер, а не защита перезапуска. Для автоматического восстановления по-прежнему нужна исправная фоновая служба.", + "startup.tray.running": "Работает", + "startup.tray.stopped": "Установлен, скрыт", + "startup.tray.stale": "Требуется ремонт", + "startup.tray.notInstalled": "Не установлен", + "startup.tray.loading": "Проверка…", + "startup.tray.unavailable": "Статус недоступен", + "startup.tray.install": "Установить и показать трей", + "startup.tray.start": "Показать значок", + "startup.tray.stop": "Закрыть значок", + "startup.tray.uninstall": "Удалить трей входа", + "startup.tray.error": "Действие Windows tray завершилось ошибкой. Подробности: ocx tray status.", "startup.recovery": "Варианты исправления", "startup.recoveryHint": "Выполните одну команду в терминале. Для Codex Desktop и Windows рекомендуется фоновая служба.", "startup.command.service": "Рекомендуется: постоянная фоновая служба", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7708ace6db6..7010c5a8f86 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -29,6 +29,7 @@ export const zh: Record = { "startup.refresh": "刷新", "startup.loading": "正在检查启动保护…", "startup.error": "无法读取启动保护状态。", + "startup.staleData": "最新启动检查失败。以下数据已过期,不能视为已受保护的证明。", "startup.status.native": "原生路由", "startup.status.protected": "已保护重启", "startup.status.atRisk": "需要处理", @@ -36,11 +37,14 @@ export const zh: Record = { "startup.summary.protected": "重启后 opencodex 会自动可用", "startup.summary.atRisk": "重启后 Codex 可能无法访问模型", "startup.riskDetail": "Codex 已指向本地代理,但没有持久服务或正常的 launcher shim 将其重新启动。", + "startup.riskDetailCustomLocal": "Codex 指向自定义本地网关。opencodex 无法管理或验证该网关的重启生命周期。", "startup.riskDetailWindowsShim": "Launcher shim 仅保护受支持的 CLI 脚本;Windows 上的 Codex Desktop 和直接 codex.exe 启动可以绕过它。", "startup.safeDetail": "当前路由与启动机制一致。重启后无需手动运行 ocx start。", "startup.routing": "Codex 路由", "startup.routing.proxy": "本地代理", "startup.routing.native": "OpenAI 原生", + "startup.routing.customLocal": "自定义本地网关", + "startup.routing.customRemote": "自定义远程网关", "startup.restartProtection": "重启保护", "startup.preference": "按需启动", "startup.enabled": "已启用", @@ -59,6 +63,25 @@ export const zh: Record = { "startup.healthy": "正常", "startup.cliOnly": "仅 CLI", "startup.stale": "已失效", + "startup.viable": "可用", + "startup.unhealthy": "已安装但异常", + "startup.conflict": "服务冲突", + "startup.installedDisabled": "已安装但禁用", + "startup.tray.title": "Windows 系统托盘", + "startup.tray.hint": "登录时启动托盘图标,一键控制代理启动、停止、重启、面板和状态。", + "startup.tray.login": "Windows 登录时启动托盘", + "startup.tray.notProtection": "托盘只是控制器,并非重启保护。无人值守恢复仍需要正常的后台服务。", + "startup.tray.running": "运行中", + "startup.tray.stopped": "已安装,未显示", + "startup.tray.stale": "需要修复", + "startup.tray.notInstalled": "未安装", + "startup.tray.loading": "正在检查…", + "startup.tray.unavailable": "状态不可用", + "startup.tray.install": "安装并显示托盘", + "startup.tray.start": "显示托盘图标", + "startup.tray.stop": "退出托盘图标", + "startup.tray.uninstall": "移除登录托盘", + "startup.tray.error": "Windows 托盘操作失败。请运行 ocx tray status 查看详情。", "startup.recovery": "修复选项", "startup.recoveryHint": "在终端运行一个命令。Codex Desktop 和 Windows 可执行文件建议使用后台服务。", "startup.command.service": "推荐:持久后台服务", diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index 53e9c917036..151bd4ef7bb 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -15,8 +15,10 @@ interface SettingsData { hostname: string; startupHealth?: { status: "native" | "protected" | "at-risk"; + routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote"; autostartEnabled: boolean; shimCoverage: "full" | "cli-only" | "none"; + diagnosticStale: boolean; }; } type SidecarBackend = "openai" | "anthropic"; @@ -626,11 +628,13 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
- {settings?.startupHealth?.status === "at-risk" && ( + {(settings?.startupHealth?.status === "at-risk" || settings?.startupHealth?.diagnosticStale) && (
- {t(settings.startupHealth.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail")}{" "} + {settings.startupHealth.diagnosticStale + ? t("startup.staleData") + : t(settings.startupHealth.routingKind === "custom-local" ? "startup.riskDetailCustomLocal" : settings.startupHealth.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail")}{" "} {t("startup.title")}
diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index 427a9b8b727..62da14ac966 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -8,17 +8,25 @@ type StartupProtection = "service" | "shim" | "none"; interface StartupHealthData { status: StartupStatus; + routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote"; routingInjected: boolean; + localRoutingDependency: boolean; autostartEnabled: boolean; rebootSafe: boolean; protection: StartupProtection; serviceInstalled: boolean; + serviceViable: boolean; + serviceEnabled: boolean; + serviceRunning: boolean; + serviceStale: boolean; + serviceConflict: boolean; serviceSupported: boolean; shimInstalled: boolean; shimHealthy: boolean; shimCoverage: "full" | "cli-only" | "none"; platform: string; recommendedCommand: string | null; + diagnosticStale: boolean; commands: { installService: string; installShim: string; @@ -26,6 +34,24 @@ interface StartupHealthData { }; } +interface TrayStatusData { + supported: boolean; + installed: boolean; + running: boolean; + stale: boolean; + summary: string; +} + +function isTrayStatusData(value: unknown): value is TrayStatusData { + if (!value || typeof value !== "object") return false; + const row = value as Record; + return typeof row.supported === "boolean" + && typeof row.installed === "boolean" + && typeof row.running === "boolean" + && typeof row.stale === "boolean" + && typeof row.summary === "string"; +} + const STATUS_KEYS: Record = { native: "startup.status.native", protected: "startup.status.protected", @@ -54,19 +80,49 @@ export default function Startup({ apiBase }: { apiBase: string }) { const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); const [copied, setCopied] = useState(null); + const [tray, setTray] = useState(null); + const [trayLoading, setTrayLoading] = useState(true); + const [trayBusy, setTrayBusy] = useState(false); + const [trayError, setTrayError] = useState(false); const refresh = useCallback(async (signal?: AbortSignal) => { setLoading(true); + setTrayLoading(true); try { const res = await fetch(`${apiBase}/api/startup-health`, { signal }); if (!res.ok) throw new Error("fetch failed"); const next = await res.json() as StartupHealthData; if (signal?.aborted) return; setData(next); - setFailed(false); + setFailed(next.diagnosticStale); + if (next.platform === "win32") { + setTrayError(false); + try { + const trayRes = await fetch(`${apiBase}/api/windows-tray`, { signal }); + if (!trayRes.ok) throw new Error("tray status failed"); + const trayNext = await trayRes.json() as unknown; + if (!isTrayStatusData(trayNext)) throw new Error("invalid tray status"); + if (!signal?.aborted) { + setTray(trayNext); + setTrayError(false); + } + } catch { + if (!signal?.aborted) { + setTray(null); + setTrayError(true); + } + } finally { + if (!signal?.aborted) setTrayLoading(false); + } + } else { + setTrayLoading(false); + } } catch { if (signal?.aborted) return; setFailed(true); + setTray(null); + setTrayError(true); + setTrayLoading(false); } finally { if (!signal?.aborted) setLoading(false); } @@ -81,6 +137,12 @@ export default function Startup({ apiBase }: { apiBase: string }) { }; }, [refresh]); + useEffect(() => { + if (!data?.diagnosticStale) return; + const timer = window.setTimeout(() => { void refresh(); }, 2000); + return () => window.clearTimeout(timer); + }, [data, refresh]); + const copyCommand = async (command: string) => { try { await navigator.clipboard.writeText(command); @@ -91,12 +153,41 @@ export default function Startup({ apiBase }: { apiBase: string }) { } }; - const statusClass = data?.status === "protected" + const runTrayAction = async (action: "install" | "start" | "stop" | "uninstall") => { + setTrayBusy(true); + setTrayError(false); + try { + const res = await fetch(`${apiBase}/api/windows-tray`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }), + }); + if (!res.ok) throw new Error("tray action failed"); + const body = await res.json() as { status: TrayStatusData }; + if (!isTrayStatusData(body.status)) throw new Error("invalid tray action status"); + setTray(body.status); + setTrayError(false); + } catch { + setTray(null); + setTrayError(true); + } finally { + setTrayBusy(false); + } + }; + + const routingKey: TKey = data?.routingKind === "opencodex-local" ? "startup.routing.proxy" + : data?.routingKind === "custom-local" ? "startup.routing.customLocal" + : data?.routingKind === "custom-remote" ? "startup.routing.customRemote" + : "startup.routing.native"; + + const statusClass = failed + ? "startup-hero--risk" + : data?.status === "protected" ? "startup-hero--safe" : data?.status === "at-risk" ? "startup-hero--risk" : "startup-hero--native"; - const StatusIcon = data?.status === "at-risk" ? IconAlert : IconCheck; + const StatusIcon = failed || data?.status === "at-risk" ? IconAlert : IconCheck; return ( <> @@ -116,15 +207,18 @@ export default function Startup({ apiBase }: { apiBase: string }) { ) : data ? ( <> + {failed &&
{t("startup.staleData")}
}
- - {t(STATUS_KEYS[data.status])} + + {t(failed ? "startup.status.atRisk" : STATUS_KEYS[data.status])} -

{t(SUMMARY_KEYS[data.status])}

-

{data.status === "at-risk" - ? t(data.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail") +

{t(failed ? "startup.error" : SUMMARY_KEYS[data.status])}

+

{failed + ? t("startup.staleData") + : data.status === "at-risk" + ? t(data.routingKind === "custom-local" ? "startup.riskDetailCustomLocal" : data.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail") : t("startup.safeDetail")}

@@ -132,7 +226,7 @@ export default function Startup({ apiBase }: { apiBase: string }) {
{t("startup.routing")}
-
{t(data.routingInjected ? "startup.routing.proxy" : "startup.routing.native")}
+
{t(routingKey)}
{t("startup.restartProtection")}
@@ -151,18 +245,60 @@ export default function Startup({ apiBase }: { apiBase: string }) {
{t("startup.service")}{t("startup.serviceHint")}
- +
{t("startup.shim")}{t("startup.shimHint")}
+ {data.platform === "win32" && ( +
+
+

{t("startup.tray.title")}

+ +
+

{t("startup.tray.hint")}

+
+
+ {t("startup.tray.login")} + {t("startup.tray.notProtection")} +
+ {trayLoading || trayError || !tray + ? {t(trayLoading ? "startup.tray.loading" : "startup.tray.unavailable")} + : } +
+
+ {!trayLoading && !trayError && tray && !tray.installed && !tray.stale && ( + + )} + {!trayLoading && !trayError && tray?.installed && !tray.stale && !tray.running && ( + + )} + {!trayLoading && !trayError && tray?.running && !tray.stale && ( + + )} + {!trayLoading && !trayError && tray?.installed && !tray.stale && ( + + )} +
+ {(trayError || tray?.stale) &&
{t("startup.tray.error")}
} +
+ )} +

{t("startup.recovery")}

@@ -170,15 +306,17 @@ export default function Startup({ apiBase }: { apiBase: string }) {

{t("startup.recoveryHint")}

-
-
- {t("startup.command.service")} - {data.commands.installService} + {data.serviceSupported && ( +
+
+ {t("startup.command.service")} + {data.commands.installService} +
+
- -
+ )}
{t("startup.command.shim")} diff --git a/gui/src/styles.css b/gui/src/styles.css index 774af239622..70f147149d8 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -731,10 +731,12 @@ dialog.modal-overlay::backdrop { .startup-state-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; } .startup-state-grid .value { font-size: var(--text-subtitle); } .startup-details, .startup-actions { margin-bottom: 16px; } +.startup-actions .panel-head > svg { width: 18px; height: 18px; flex: 0 0 auto; } .startup-detail-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid var(--border-soft); } .startup-detail-row > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; } .startup-detail-row span:not(.badge) { color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); } .startup-actions > .muted { margin: -4px 0 14px; font-size: var(--text-control); } +.startup-tray-buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; } .startup-command-list { border: 1px solid var(--border-soft); border-radius: var(--radius-sm); overflow: hidden; } .startup-command-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px; } .startup-command-row + .startup-command-row { border-top: 1px solid var(--border-soft); } diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index a5fe2d7a16c..d31939be51f 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -335,7 +335,7 @@ export async function runDoctor(): Promise { const startup = collectStartupHealth(readConfigDiagnostics().config); console.log("\nCodex restart safety"); console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`); - console.log(` routing=${startup.routingInjected ? "opencodex" : "native"}, service=${startup.serviceInstalled ? "installed" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`); + console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`); const currentProxyEnv = collectProxyEnv(); const configuredProxy = collectConfiguredProxy(); diff --git a/src/cli/help.ts b/src/cli/help.ts index 19027802202..579a87fd9a9 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -49,6 +49,14 @@ const helpEntries: Record = { summary: "Auto-start the proxy when `codex` launches.", details: ["Use `remove` as an alias for `uninstall`."], }, + tray: { + usage: "ocx tray [--json]", + summary: "Install and control the Windows status tray icon.", + details: [ + "The tray starts at Windows login and provides one-click proxy controls.", + "Tray start/stop controls the icon only; use its menu to start or stop the proxy.", + ], + }, ensure: { usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." }, sync: { usage: "ocx sync", summary: "Fetch provider models and inject them into Codex config." }, "sync-cache": { usage: "ocx sync-cache", summary: "Refresh Codex's model cache from the active catalog." }, @@ -164,6 +172,7 @@ Usage: ocx uninstall Remove service/shim/config and restore native Codex (alias: remove) ocx service [sub] Run as a background service (default: install/update/start) ocx codex-shim Auto-start proxy when \`codex\` launches (install|status|uninstall|remove) + ocx tray Windows status tray (install|start|stop|status|uninstall) ocx ensure Ensure the proxy is running and Codex config/cache are current ocx sync Fetch models from providers and inject into Codex config ocx sync-cache Refresh Codex's model cache from the active catalog diff --git a/src/cli/index.ts b/src/cli/index.ts index 2a1571b5606..9455cf9de15 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -26,7 +26,7 @@ import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSele import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness"; import { stopProxy } from "../lib/process-control"; import { loadServiceTokenFromFile } from "../lib/service-secrets"; -import { serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service"; +import { diagnoseService, serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service"; import { startupHealthSummary } from "../codex/autostart-health"; import { drainAndShutdown, startServer } from "../server"; import { injectSystemEnv, revertSystemEnv } from "../server/system-env"; @@ -303,6 +303,50 @@ async function handleEnsure() { console.log(`✅ Proxy running on port ${port}`); } +/** Fixed tray action: start the proxy without depending on codexAutoStart. */ +async function handleTrayProxyStart(): Promise { + const live = await findLiveProxy(); + if (live) { + console.log(`Proxy already running on port ${live.port}.`); + return; + } + const service = diagnoseService(); + const { serviceStartableFromTray } = await import("../service"); + const serviceStartable = serviceStartableFromTray(service); + if (service.installed && !serviceStartable) { + console.error(`Cannot start from the tray because the installed service is not viable: ${service.summary}`); + console.error("Repair or remove the service before starting a direct proxy."); + process.exitCode = 1; + return; + } + if (serviceStartable) { + await serviceCommand("start"); + } else { + const config = loadConfig(); + const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100; + const child = spawn(process.execPath, startArgv(port), { + detached: true, + stdio: "ignore", + windowsHide: true, + env: { ...process.env, OCX_SERVICE: "1" }, + }); + child.unref(); + } + const started = await waitForProxy(); + if (!started) { + console.error("Proxy did not become healthy after the tray start action."); + process.exitCode = 1; + return; + } + console.log(`Proxy running on port ${started.port}.`); +} + +async function handleTrayProxyRestart(): Promise { + await handleStop(); + if (process.exitCode && process.exitCode !== 0) return; + await handleTrayProxyStart(); +} + async function handleStop() { const stoppedService = stopServiceIfInstalled(); if (stoppedService) console.log("🛑 Service manager stopped (won't respawn)."); @@ -383,6 +427,14 @@ async function handleUninstall() { await runStep("service removed", () => uninstallServiceIfInstalled()); + if (process.platform === "win32") { + await runStep("Windows tray removed", async () => { + const { getWindowsTrayStatus, uninstallWindowsTray } = await import("../tray/windows"); + if (!getWindowsTrayStatus().installed && !getWindowsTrayStatus().stale) return false; + uninstallWindowsTray(); + }); + } + await runStep("native Codex restored", () => { const r = restoreNativeCodex(); if (!r.success) throw new Error(r.message); @@ -596,6 +648,11 @@ switch (command) { case "service": await serviceCommand(...args.slice(1)); break; + case "tray": { + const { windowsTrayCommand } = await import("../tray/windows"); + await windowsTrayCommand(args.slice(1)); + break; + } case "codex-shim": { const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim"); switch (args[1]) { @@ -638,6 +695,22 @@ switch (command) { await refreshVersionCache(channel); break; } + case "__tray-start": + await handleTrayProxyStart(); + break; + case "__tray-restart": + await handleTrayProxyRestart(); + break; + case "__tray-host": { + const { runWindowsTrayHost } = await import("../tray/windows"); + await runWindowsTrayHost(); + break; + } + case "__startup-health": { + const { collectStartupHealth } = await import("../codex/autostart-health"); + console.log(JSON.stringify(collectStartupHealth(loadConfig()))); + break; + } case "__gui-update-worker": { const jobId = args[1]; if (!jobId) process.exit(1); diff --git a/src/cli/status.ts b/src/cli/status.ts index c6973dce2be..5c32b897065 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -3,9 +3,9 @@ import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor"; import { isOpencodexHealthz, probeHostname } from "../server/proxy-liveness"; import type { OcxConfig } from "../types"; -import { serviceStatusSummary } from "../service"; +import { diagnoseService } from "../service"; import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health"; -import { isCodexRoutingInjected } from "../codex/inject"; +import { getCodexRoutingKind } from "../codex/inject"; import { diagnoseCodexShim } from "../codex/shim"; type HealthCheck = { @@ -119,14 +119,20 @@ export async function collectStatus(): Promise { const listen = selectListenTarget(config, pid, pid ? readRuntimePort(pid) : null); const health = await checkProxyHealth(listen); const bunRuntime = durableBunRuntime(); - const serviceSummary = serviceStatusSummary(); + const service = diagnoseService(); + const serviceSummary = service.summary; const codexShim = diagnoseCodexShim(); const codexShimSummary = codexShim.summary; const startup = deriveStartupHealth({ - routingInjected: isCodexRoutingInjected(), + routingKind: getCodexRoutingKind(), autostartEnabled: codexAutoStartEnabled(config), - serviceInstalled: serviceSummary.startsWith("installed"), - serviceSupported: !serviceSummary.startsWith("unsupported"), + serviceInstalled: service.installed, + serviceViable: service.viable, + serviceEnabled: service.enabled, + serviceRunning: service.running, + serviceStale: service.stale, + serviceConflict: service.conflict, + serviceSupported: service.supported, shimInstalled: codexShim.installed, shimHealthy: codexShim.healthy, platform: process.platform, diff --git a/src/codex/autostart-health.ts b/src/codex/autostart-health.ts index 82a3354abcd..3d0f26cfdd9 100644 --- a/src/codex/autostart-health.ts +++ b/src/codex/autostart-health.ts @@ -1,7 +1,7 @@ import { codexAutoStartEnabled } from "../config"; -import { serviceStatusSummary } from "../service"; +import { diagnoseService } from "../service"; import type { OcxConfig } from "../types"; -import { isCodexRoutingInjected } from "./inject"; +import { getCodexRoutingKind, type CodexRoutingKind } from "./inject"; import { diagnoseCodexShim } from "./shim"; export type StartupProtection = "service" | "shim" | "none"; @@ -9,27 +9,41 @@ export type StartupHealthStatus = "native" | "protected" | "at-risk"; export type ShimCoverage = "full" | "cli-only" | "none"; export interface StartupHealthInputs { - routingInjected: boolean; + routingKind: CodexRoutingKind; autostartEnabled: boolean; serviceInstalled: boolean; + serviceViable: boolean; + serviceEnabled: boolean; + serviceRunning: boolean; + serviceStale: boolean; + serviceConflict: boolean; serviceSupported: boolean; shimInstalled: boolean; shimHealthy: boolean; platform: NodeJS.Platform; + diagnosticStale?: boolean; } export interface StartupHealth { status: StartupHealthStatus; + routingKind: CodexRoutingKind; routingInjected: boolean; + localRoutingDependency: boolean; autostartEnabled: boolean; rebootSafe: boolean; protection: StartupProtection; serviceInstalled: boolean; + serviceViable: boolean; + serviceEnabled: boolean; + serviceRunning: boolean; + serviceStale: boolean; + serviceConflict: boolean; shimInstalled: boolean; shimHealthy: boolean; shimCoverage: ShimCoverage; serviceSupported: boolean; platform: NodeJS.Platform; + diagnosticStale: boolean; recommendedCommand: string | null; commands: { installService: string; @@ -46,34 +60,39 @@ const COMMANDS = { export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth { const shimEffective = inputs.autostartEnabled && inputs.shimHealthy; - // Windows Codex Desktop and exact codex.exe invocations bypass script wrappers. - // A healthy shim protects supported CLI launchers there, but not the global - // openai_base_url consumed by every Codex surface. + const routingInjected = inputs.routingKind === "opencodex-local"; + const localRoutingDependency = inputs.routingKind === "opencodex-local" || inputs.routingKind === "custom-local"; + // Script launchers never cover Codex Desktop/app-server surfaces. This is + // intentionally conservative on every OS and for WSL-shared Codex homes. const shimCoverage: ShimCoverage = !shimEffective ? "none" - : inputs.platform === "win32" - ? "cli-only" - : "full"; - const protection: StartupProtection = inputs.serviceInstalled + : "cli-only"; + // We can only credit an opencodex service/shim for routing that opencodex owns. + // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair. + const ownsLocalRouting = inputs.routingKind === "opencodex-local"; + const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable ? "service" - : shimEffective + : ownsLocalRouting && shimEffective ? "shim" : "none"; - const rebootSafe = !inputs.routingInjected || inputs.serviceInstalled || shimCoverage === "full"; - const status: StartupHealthStatus = !inputs.routingInjected + const rebootSafe = !localRoutingDependency || (ownsLocalRouting && inputs.serviceViable); + const status: StartupHealthStatus = !localRoutingDependency ? "native" : rebootSafe ? "protected" : "at-risk"; const recommendedCommand = status !== "at-risk" ? null + : inputs.routingKind === "custom-local" + ? COMMANDS.restoreNative : inputs.serviceSupported ? COMMANDS.installService - : inputs.autostartEnabled - ? COMMANDS.installShim - : COMMANDS.restoreNative; + : COMMANDS.restoreNative; return { ...inputs, + diagnosticStale: inputs.diagnosticStale ?? false, + routingInjected, + localRoutingDependency, status, rebootSafe, protection, @@ -86,12 +105,17 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth /** Collect current machine state without mutating config, services, or shims. */ export function collectStartupHealth(config: Pick): StartupHealth { const shim = diagnoseCodexShim(); - const serviceSummary = serviceStatusSummary(); + const service = diagnoseService(); return deriveStartupHealth({ - routingInjected: isCodexRoutingInjected(), + routingKind: getCodexRoutingKind(), autostartEnabled: codexAutoStartEnabled(config), - serviceInstalled: serviceSummary.startsWith("installed"), - serviceSupported: !serviceSummary.startsWith("unsupported"), + serviceInstalled: service.installed, + serviceViable: service.viable, + serviceEnabled: service.enabled, + serviceRunning: service.running, + serviceStale: service.stale, + serviceConflict: service.conflict, + serviceSupported: service.supported, shimInstalled: shim.installed, shimHealthy: shim.healthy, platform: process.platform, @@ -99,9 +123,14 @@ export function collectStartupHealth(config: Pick): } export function startupHealthSummary(health: StartupHealth): string { - if (health.status === "native") return "native Codex routing (no opencodex restart dependency)"; + if (health.status === "native") return health.routingKind === "custom-remote" + ? "custom remote Codex routing (no local restart dependency)" + : "native Codex routing (no opencodex restart dependency)"; if (health.protection === "service") return "protected by background service"; - if (health.shimCoverage === "full") return "protected by Codex launcher shim"; + if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${health.commands.restoreNative}' to restore native routing)`; if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; - return `AT RISK after restart (no service or healthy shim; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + return `AT RISK after restart (no viable background service; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; } diff --git a/src/codex/inject.ts b/src/codex/inject.ts index aeba43cd181..5e8c19328e0 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -161,6 +161,63 @@ export function hasInjectedOpenaiBaseUrl(content: string): boolean { return false; } +export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote"; + +function rootTomlString(content: string, key: string): string | null { + const lines = content.split("\n"); + const firstTable = lines.findIndex(line => /^\s*\[/.test(line)); + const rootLines = lines.slice(0, firstTable === -1 ? lines.length : firstTable); + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); + for (const line of rootLines) { + const match = pattern.exec(line); + if (match?.[1]) return match[1].trim(); + } + return null; +} + +function providerTableString(content: string, provider: string, key: string): string | null { + const lines = content.split("\n"); + const header = `[model_providers.${provider}]`; + const start = lines.findIndex(line => line.trim() === header); + if (start === -1) return null; + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); + for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) { + const match = pattern.exec(lines[index]); + if (match?.[1]) return match[1].trim(); + } + return null; +} + +function isLoopbackUrl(value: string): boolean { + try { + const hostname = new URL(value).hostname.toLowerCase(); + return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; + } catch { + return false; + } +} + +/** Classify actual routing dependency separately from opencodex ownership. */ +export function classifyCodexRouting(content: string): CodexRoutingKind { + const rootBaseUrl = rootTomlString(content, "openai_base_url"); + if (rootBaseUrl) { + if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local"; + if (!isLoopbackUrl(rootBaseUrl)) return "custom-remote"; + return "custom-local"; + } + const rootProvider = rootTomlString(content, "model_provider"); + if (rootProvider) { + const providerBaseUrl = providerTableString(content, rootProvider, "base_url"); + if (providerBaseUrl) { + if (rootProvider === "opencodex") return "opencodex-local"; + return isLoopbackUrl(providerBaseUrl) ? "custom-local" : "custom-remote"; + } + } + return "native"; +} + /** * True when the active Codex config is owned by opencodex routing. Covers the * loopback Design B root override and the legacy/non-loopback provider table. @@ -168,8 +225,8 @@ export function hasInjectedOpenaiBaseUrl(content: string): boolean { */ export function hasInjectedCodexRouting(content: string): boolean { if (hasInjectedOpenaiBaseUrl(content)) return true; - return /^\s*model_provider\s*=\s*["']opencodex["']\s*$/m.test(content) - && /^\s*\[model_providers\.opencodex\]\s*$/m.test(content); + return rootTomlString(content, "model_provider") === "opencodex" + && providerTableString(content, "opencodex", "base_url") !== null; } /** Read-only probe used by status, doctor, and the dashboard. */ @@ -183,6 +240,16 @@ export function isCodexRoutingInjected(): boolean { } } +export function getCodexRoutingKind(): CodexRoutingKind { + const path = CODEX_CONFIG_PATH; + if (!existsSync(path)) return "native"; + try { + return classifyCodexRouting(readFileSync(path, "utf8")); + } catch { + return "native"; + } +} + /** * Strip every existing `model_provider` line that we must not duplicate: any line set to * "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any diff --git a/src/codex/shim.ts b/src/codex/shim.ts index e418d3d48e4..c3216d71150 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -88,6 +88,17 @@ function isShim(path: string): boolean { } } +function isHealthyShim(path: string, platform: NodeJS.Platform): boolean { + try { + const content = readFileSync(path, "utf8"); + if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false; + if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false; + return true; + } catch { + return false; + } +} + /** * A PATH entry that reaches Windows through WSL drive interop * (`//...`; root defaults to /mnt, configurable via @@ -345,7 +356,25 @@ exit $LASTEXITCODE function readState(): ShimState | null { try { - return JSON.parse(readFileSync(statePath(), "utf8")) as ShimState; + const value = JSON.parse(readFileSync(statePath(), "utf8")) as unknown; + if (!value || typeof value !== "object") return null; + const state = value as Record; + if (typeof state.platform !== "string") return null; + const validFile = (item: unknown): item is ShimFileState => { + if (!item || typeof item !== "object") return false; + const file = item as Record; + return typeof file.wrapperPath === "string" + && typeof file.originalPath === "string" + && typeof file.backupPath === "string" + && (file.realPath === undefined || typeof file.realPath === "string") + && (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean"); + }; + if (state.wrappers !== undefined) { + if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return null; + } else if (!validFile(state)) { + return null; + } + return state as unknown as ShimState; } catch { return null; } @@ -517,6 +546,13 @@ export interface CodexShimDiagnostic { export function diagnoseCodexShim(): CodexShimDiagnostic { const state = readState(); if (!state) { + if (existsSync(statePath())) { + return { + installed: true, + healthy: false, + summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`, + }; + } return { installed: false, healthy: false, @@ -528,7 +564,7 @@ export function diagnoseCodexShim(): CodexShimDiagnostic { ? existsSync(file.backupPath) && !existsSync(file.originalPath) : existsSync(file.wrapperPath) && (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false)) - && isShim(file.wrapperPath)); + && isHealthyShim(file.wrapperPath, state.platform)); const summary = files.map(file => { const wrapper = existsSync(file.wrapperPath) ? isShim(file.wrapperPath) diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 08cab23e228..46fe10e2a0b 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -55,7 +55,8 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; import { applySystemEnvToggle } from "../system-env"; -import { collectStartupHealth } from "../../codex/autostart-health"; +import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; +import { runWindowsTrayAction } from "../windows-tray-control"; import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared"; import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; @@ -76,12 +77,36 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise | null = null; +let generation = 0; + +export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupHealth { + if (!value.localRoutingDependency) return { ...value, diagnosticStale: true }; + return { + ...value, + status: "at-risk", + rebootSafe: false, + protection: "none", + diagnosticStale: true, + recommendedCommand: value.routingKind === "custom-local" ? value.commands.restoreNative : value.commands.installService, + }; +} + +function conservativeFallback(config: Pick): StartupHealth { + const shim = diagnoseCodexShim(); + return deriveStartupHealth({ + routingKind: getCodexRoutingKind(), + autostartEnabled: codexAutoStartEnabled(config), + serviceInstalled: false, + serviceViable: false, + serviceEnabled: false, + serviceRunning: false, + serviceStale: false, + serviceConflict: false, + serviceSupported: process.platform === "win32" || process.platform === "darwin" || process.platform === "linux", + shimInstalled: shim.installed, + shimHealthy: shim.healthy, + platform: process.platform, + diagnosticStale: true, + }); +} + +function runProbe(config: Pick): Promise { + const bun = durableBunPath(); + const cli = join(import.meta.dir, "..", "cli", "index.ts"); + return new Promise(resolve => { + execFile(bun, [cli, "__startup-health"], { + encoding: "utf8", + env: process.env, + timeout: PROBE_TIMEOUT_MS, + windowsHide: true, + maxBuffer: 256 * 1024, + }, (error, stdout) => { + if (!error) { + const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean); + for (let index = lines.length - 1; index >= 0; index -= 1) { + try { + const parsed = JSON.parse(lines[index]) as StartupHealth; + if (["native", "protected", "at-risk"].includes(parsed.status) && typeof parsed.rebootSafe === "boolean") { + resolve({ ...parsed, diagnosticStale: false }); + return; + } + } catch { /* scan earlier output; config repair messages may precede JSON */ } + } + } + resolve(cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config)); + }); + }); +} + +function refreshInBackground(config: Pick): void { + if (inflight) return; + const startedGeneration = generation; + const probe = runProbe(config).then(value => { + if (startedGeneration === generation) cached = { timestamp: Date.now(), value }; + return value; + }); + inflight = probe.finally(() => { + if (inflight === probe || startedGeneration === generation) inflight = null; + }); +} + +/** Stale-while-revalidate: service-manager probes never hold open a model/UI request. */ +export async function getCachedStartupHealth(config: Pick): Promise { + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.value; + refreshInBackground(config); + return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config); +} + +export function invalidateStartupHealthCache(): void { + generation += 1; + cached = null; + inflight = null; +} diff --git a/src/server/windows-tray-control.ts b/src/server/windows-tray-control.ts new file mode 100644 index 00000000000..bc59ef894c3 --- /dev/null +++ b/src/server/windows-tray-control.ts @@ -0,0 +1,41 @@ +import { execFile } from "node:child_process"; +import { join } from "node:path"; +import { durableBunPath } from "../lib/bun-runtime"; +import type { WindowsTrayStatus } from "../tray/windows"; + +export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall"; + +/** Run blocking registry/PowerShell waits outside the proxy event loop. */ +export function runWindowsTrayAction(action: WindowsTrayAction): Promise { + if (action === "status") { + return import("../tray/windows").then(({ getWindowsTrayStatusAsync }) => getWindowsTrayStatusAsync()); + } + const bun = durableBunPath(); + const cli = join(import.meta.dir, "..", "cli", "index.ts"); + return new Promise((resolve, reject) => { + execFile(bun, [cli, "tray", action, "--json"], { + encoding: "utf8", + env: process.env, + timeout: 30_000, + windowsHide: true, + maxBuffer: 256 * 1024, + }, (error, stdout, stderr) => { + const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean); + for (let index = lines.length - 1; index >= 0; index -= 1) { + try { + const parsed = JSON.parse(lines[index]) as WindowsTrayStatus | { error?: unknown }; + if ("supported" in parsed && typeof parsed.supported === "boolean") { + resolve(parsed); + return; + } + const failure = parsed as { error?: unknown }; + if (typeof failure.error === "string") { + reject(new Error(failure.error)); + return; + } + } catch { /* scan earlier lines */ } + } + reject(new Error(error?.message || stderr.trim() || "Windows tray action returned no status")); + }); + }); +} diff --git a/src/service.ts b/src/service.ts index c2006c0c090..dcb28dc22df 100644 --- a/src/service.ts +++ b/src/service.ts @@ -451,6 +451,30 @@ export function buildWindowsTaskXml(script = windowsServiceScriptPath(), launche `; } +function taskXmlSection(xml: string, tag: string): string { + return new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, "i").exec(xml)?.[1] ?? ""; +} + +/** Validate the security/lifecycle-critical fields of the registered scheduler task. */ +export function windowsTaskRegistrationHealthy( + xml: string, + wscript = windowsWscript(), + launcher = windowsLauncherVbsPath(), +): boolean { + const trigger = taskXmlSection(xml, "LogonTrigger"); + const principal = taskXmlSection(xml, "Principal"); + const settings = taskXmlSection(xml, "Settings"); + const action = taskXmlSection(xml, "Exec"); + return /\s*true\s*<\/Enabled>/i.test(trigger) + && /\s*InteractiveToken\s*<\/LogonType>/i.test(principal) + && /\s*LeastPrivilege\s*<\/RunLevel>/i.test(principal) + && /\s*true\s*<\/Enabled>/i.test(settings) + && /\s*IgnoreNew\s*<\/MultipleInstancesPolicy>/i.test(settings) + && /\s*PT0S\s*<\/ExecutionTimeLimit>/i.test(settings) + && action.includes(`${taskXmlString(wscript)}`) + && action.includes(`${taskXmlString(`/b /nologo "${launcher}"`)}`); +} + // ── macOS (launchd) ── function installLaunchd(): void { const dir = join(homedir(), "Library", "LaunchAgents"); @@ -561,6 +585,7 @@ async function installWindowsNative(): Promise { function startWindows(): void { schtasks(["/run", "/tn", TASK]); } function stopWindows(): void { try { schtasks(["/end", "/tn", TASK]); } catch { /* not running */ } } function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } } +function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } } function uninstallWindows(): void { try { schtasks(["/delete", "/tn", TASK, "/f"]); } catch { /* absent */ } if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath()); @@ -814,31 +839,131 @@ export function uninstallServiceIfInstalled(): boolean { /** True if a background service (launchd/systemd/Task Scheduler) is installed. */ export function isServiceInstalled(): boolean { - return serviceStatusSummary().startsWith("installed"); + return diagnoseService().installed; +} + +export interface ServiceDiagnostic { + supported: boolean; + installed: boolean; + enabled: boolean; + running: boolean; + viable: boolean; + startable: boolean; + stale: boolean; + conflict: boolean; + backend: ServiceBackend | "launchd" | "systemd" | null; + summary: string; +} + +/** Windows tray may restart a healthy-but-stopped native service; stale/conflicting installs remain blocked. */ +export function serviceStartableFromTray(service: ServiceDiagnostic): boolean { + return service.startable && !service.stale && !service.conflict; +} + +export interface WindowsServiceDiagnosticInputs { + schedulerInstalled: boolean; + schedulerEnabled: boolean; + schedulerAssetsHealthy: boolean; + nativeStatus: "started" | "stopped" | "nonexistent" | "unknown"; + staleBakedPaths: boolean; + nativeRepairAssetsOnly: boolean; + diagnostics: string; +} + +export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticInputs): ServiceDiagnostic { + const nativeInstalled = inputs.nativeStatus !== "nonexistent"; + const conflict = inputs.schedulerInstalled && nativeInstalled; + const stale = inputs.staleBakedPaths + || (inputs.schedulerInstalled && !inputs.schedulerAssetsHealthy) + || (inputs.nativeStatus === "nonexistent" && inputs.nativeRepairAssetsOnly); + const backend = inputs.schedulerInstalled ? "scheduler" : nativeInstalled ? "native" : null; + const enabled = inputs.schedulerInstalled ? inputs.schedulerEnabled : inputs.nativeStatus === "started"; + const running = nativeInstalled ? inputs.nativeStatus === "started" : inputs.schedulerInstalled && inputs.schedulerEnabled; + const viable = !conflict && !stale + && (inputs.schedulerInstalled ? inputs.schedulerEnabled && inputs.schedulerAssetsHealthy : inputs.nativeStatus === "started"); + const startable = !conflict && !stale + && (inputs.schedulerInstalled + ? inputs.schedulerEnabled && inputs.schedulerAssetsHealthy + : inputs.nativeStatus === "started" || inputs.nativeStatus === "stopped"); + const detail = conflict + ? "CONFLICT: Task Scheduler and native WinSW are both present — run 'ocx service uninstall' then reinstall one" + : stale + ? "stale or missing service assets — run 'ocx service install' to repair" + : inputs.schedulerInstalled + ? inputs.schedulerEnabled ? "Task Scheduler enabled" : "Task Scheduler disabled" + : nativeInstalled + ? `native (WinSW ${WINSW_VERSION}): ${inputs.nativeStatus}` + : "not installed"; + const summary = backend ? `installed, ${detail} (${inputs.diagnostics})` : `not installed (${inputs.diagnostics})`; + return { + supported: true, + installed: inputs.schedulerInstalled || nativeInstalled, + enabled, + running, + viable, + startable, + stale, + conflict, + backend, + summary, + }; } -export function serviceStatusSummary(): string { +/** + * Fail-closed restart diagnostic. Presence alone is never enough: conflicting + * managers, stale baked paths, disabled registrations, and unknown/stopped + * native managers cannot claim that Codex will reconnect after a reboot. + */ +export function diagnoseService(): ServiceDiagnostic { const diagnostics = serviceDiagnosticsSummary(); if (process.platform === "darwin") { - if (!existsSync(plistPath())) return `not installed (${diagnostics})`; - const status = statusLaunchd(); - return status ? `installed (launchd; ${diagnostics})` : `installed, not loaded (${diagnostics})`; + const installed = existsSync(plistPath()); + const running = installed && Boolean(statusLaunchd()); + const stale = installed && bakedServicePathsDiagnostic() !== null; + const viable = installed && running && !stale; + const summary = !installed ? `not installed (${diagnostics})` + : stale ? `installed, but stale (launchd; ${diagnostics})` + : running ? `installed and loaded (launchd; ${diagnostics})` + : `installed, not loaded (launchd; ${diagnostics})`; + return { supported: true, installed, enabled: running, running, viable, startable: installed && !stale, stale, conflict: false, backend: "launchd", summary }; } if (process.platform === "win32") { - const scheduler = statusWindows(); - const native = winswStatusSummary(); - if (scheduler && native) return `installed (CONFLICT: Task Scheduler AND native WinSW both present — run 'ocx service uninstall' then reinstall one; ${diagnostics})`; - if (native) return `installed (${native}; ${diagnostics})`; - return scheduler ? `installed (Task Scheduler; ${diagnostics})` : `not installed (${diagnostics})`; + const schedulerXml = statusWindowsXml(); + const schedulerInstalled = schedulerXml.length > 0; + const schedulerSettings = taskXmlSection(schedulerXml, "Settings"); + const schedulerEnabled = schedulerInstalled && /\s*true\s*<\/Enabled>/i.test(schedulerSettings); + const schedulerAssets = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()].every(existsSync) + && windowsTaskRegistrationHealthy(schedulerXml); + const nativeStatus = statusWinswRaw(); + return deriveWindowsServiceDiagnostic({ + schedulerInstalled, + schedulerEnabled, + schedulerAssetsHealthy: schedulerAssets, + nativeStatus, + staleBakedPaths: bakedServicePathsDiagnostic() !== null, + nativeRepairAssetsOnly: Boolean(winswStatusSummary()), + diagnostics, + }); } if (process.platform === "linux") { - if (existsSync("/.dockerenv")) return "unsupported in Docker"; - if (!isSystemd()) return "unsupported: systemd not found"; - if (!existsSync(unitPath())) return `not installed (${diagnostics})`; - const status = statusSystemd(); - return status ? `installed (systemd user; ${diagnostics})` : `installed, not running (${diagnostics})`; + if (existsSync("/.dockerenv")) return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: "unsupported in Docker" }; + if (!isSystemd()) return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: "unsupported: systemd not found" }; + const installed = existsSync(unitPath()); + const enabled = installed && (() => { try { return sh(`systemctl --user is-enabled ${TASK}`) === "enabled"; } catch { return false; } })(); + const running = installed && (() => { try { return sh(`systemctl --user is-active ${TASK}`) === "active"; } catch { return false; } })(); + const stale = installed && bakedServicePathsDiagnostic() !== null; + const viable = installed && enabled && running && !stale; + const summary = !installed ? `not installed (${diagnostics})` + : stale ? `installed, but stale (systemd user; ${diagnostics})` + : viable ? `installed, enabled and running (systemd user; ${diagnostics})` + : `installed, but ${!enabled ? "disabled" : "not running"} (systemd user; ${diagnostics})`; + return { supported: true, installed, enabled, running, viable, startable: installed && !stale, stale, conflict: false, backend: "systemd", summary }; } - return `unsupported on ${process.platform}`; + return { supported: false, installed: false, enabled: false, running: false, viable: false, startable: false, stale: false, conflict: false, backend: null, summary: `unsupported on ${process.platform}` }; +} + +export function serviceStatusSummary(): string { + return diagnoseService().summary; } export function normalizeServiceSubcommand(sub?: string): string { diff --git a/src/tray/windows-tray.ps1 b/src/tray/windows-tray.ps1 new file mode 100644 index 00000000000..a9994d35ff9 --- /dev/null +++ b/src/tray/windows-tray.ps1 @@ -0,0 +1,273 @@ +param( + [Parameter(Mandatory = $true)][string]$BunPath, + [Parameter(Mandatory = $true)][string]$CliPath, + [Parameter(Mandatory = $true)][string]$CodexHome, + [Parameter(Mandatory = $true)][string]$OpenCodexHome, + [ValidateSet("Run", "Stop")][string]$Mode = "Run", + [int]$HostPid = 0 +) + +$ErrorActionPreference = "Stop" +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +# Normalize aliases before deriving singleton/event names. Without this, +# C:\path and C:\path\. create separate tray instances for the same home. +function Normalize-HomePath([string]$Value) { + $full = [System.IO.Path]::GetFullPath($Value) + $root = [System.IO.Path]::GetPathRoot($full) + if ($full -eq $root) { return $full } + return $full.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) +} +$OpenCodexHome = Normalize-HomePath $OpenCodexHome +$CodexHome = Normalize-HomePath $CodexHome + +function Get-StableHash([string]$Value) { + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Value.ToLowerInvariant()) + $hash = [System.BitConverter]::ToString($sha.ComputeHash($bytes)).Replace("-", "").Substring(0, 20) + return $hash + } finally { + $sha.Dispose() + } +} + +$stableHash = Get-StableHash $OpenCodexHome +$stopEventCreated = $false +$stopEvent = New-Object System.Threading.EventWaitHandle($false, [System.Threading.EventResetMode]::AutoReset, "Local\OpenCodexTrayStop-$stableHash", [ref]$stopEventCreated) +if ($Mode -eq "Stop") { + [void]$stopEvent.Set() + $stopEvent.Dispose() + exit 0 +} + +$createdNew = $false +$mutex = New-Object System.Threading.Mutex($true, "Local\OpenCodexTray-$stableHash", [ref]$createdNew) +if (-not $createdNew) { + $stopEvent.Dispose() + $mutex.Dispose() + exit 0 +} + +$heartbeatPath = Join-Path $OpenCodexHome "tray-heartbeat.json" +$actionLogPath = Join-Path $OpenCodexHome "tray-actions.log" + +function Write-ActionLog([string]$Message) { + $line = "[$([DateTimeOffset]::Now.ToString('o'))] $Message" + [System.IO.File]::AppendAllText($actionLogPath, $line + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false))) +} + +function ConvertTo-NativeArgument([string]$Value) { + if ($Value.Contains('"') -or $Value.Contains("`r") -or $Value.Contains("`n")) { + throw "Invalid native command argument" + } + return '"' + $Value + '"' +} + +function Start-OcxCommand([string[]]$CommandArgs) { + try { + $allArgs = @($CliPath) + $CommandArgs + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $BunPath + $psi.Arguments = (($allArgs | ForEach-Object { ConvertTo-NativeArgument $_ }) -join " ") + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden + $psi.EnvironmentVariables["CODEX_HOME"] = $CodexHome + $psi.EnvironmentVariables["OPENCODEX_HOME"] = $OpenCodexHome + $process = [System.Diagnostics.Process]::Start($psi) + if ($null -ne $process) { $process.Dispose() } + Write-ActionLog "dispatched $($CommandArgs -join ' ')" + return $true + } catch { + Write-ActionLog "launch failed: $($_.Exception.GetType().Name)" + $notify.ShowBalloonTip(5000, "opencodex action failed", "The action could not start. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error) + return $false + } +} + +function Read-ListenTarget { + foreach ($path in @((Join-Path $OpenCodexHome "runtime-port.json"), (Join-Path $OpenCodexHome "config.json"))) { + try { + $value = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json + $candidate = [int]$value.port + if ($candidate -gt 0 -and $candidate -le 65535) { + $candidateHost = [string]$value.hostname + $ip = $null + $hostName = if ([string]::IsNullOrWhiteSpace($candidateHost) -or $candidateHost -in @("localhost", "0.0.0.0", "::", "[::]")) { + "127.0.0.1" + } elseif ([System.Net.IPAddress]::TryParse($candidateHost.Trim("[", "]"), [ref]$ip)) { + if ($ip.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) { "[$($ip.ToString())]" } else { $ip.ToString() } + } else { + "127.0.0.1" + } + return @{ port = $candidate; host = $hostName; pid = $value.pid } + } + } catch { } + } + return @{ port = 10100; host = "127.0.0.1"; pid = $null } +} + +function Read-JsonUrl([string]$Url) { + $request = [System.Net.HttpWebRequest]::Create($Url) + $request.Method = "GET" + $request.Timeout = 700 + $request.ReadWriteTimeout = 700 + $response = $request.GetResponse() + try { + $reader = New-Object System.IO.StreamReader($response.GetResponseStream()) + try { return ($reader.ReadToEnd() | ConvertFrom-Json) } finally { $reader.Dispose() } + } finally { + $response.Dispose() + } +} + +$notify = New-Object System.Windows.Forms.NotifyIcon +$menu = New-Object System.Windows.Forms.ContextMenuStrip +$statusItem = New-Object System.Windows.Forms.ToolStripMenuItem +$statusItem.Enabled = $false +$safetyItem = New-Object System.Windows.Forms.ToolStripMenuItem +$safetyItem.Enabled = $false +$openItem = $menu.Items.Add("Open Dashboard") +$startItem = $menu.Items.Add("Start Proxy") +$stopItem = $menu.Items.Add("Stop Proxy and Restore Native Routing") +$restartItem = $menu.Items.Add("Restart Proxy") +[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) +[void]$menu.Items.Add($statusItem) +[void]$menu.Items.Add($safetyItem) +$logsItem = $menu.Items.Add("Open Logs Folder") +[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator)) +$exitItem = $menu.Items.Add("Exit Tray") + +$script:online = $false +$script:port = 10100 +$script:proxyPid = $null +$script:pendingAction = $null +$script:pendingStarted = 0L +$script:pendingDeadline = 0L +$script:pendingOldProxyPid = $null + +function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) { + $script:pendingAction = $Action + $script:pendingStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + $script:pendingDeadline = $script:pendingStarted + ($TimeoutSeconds * 1000) + $script:pendingOldProxyPid = $script:proxyPid +} + +function Complete-PendingAction([bool]$Success) { + if ($null -eq $script:pendingAction) { return } + $action = $script:pendingAction + $script:pendingAction = $null + if ($Success) { + Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))" + $notify.ShowBalloonTip(2500, "opencodex", "$action completed.", [System.Windows.Forms.ToolTipIcon]::Info) + } else { + Write-ActionLog "$action failed to reach the expected state" + $notify.ShowBalloonTip(5000, "opencodex action failed", "$action did not reach the expected state. Open the logs folder or run ocx doctor.", [System.Windows.Forms.ToolTipIcon]::Error) + } +} + +function Update-TrayState { + $target = Read-ListenTarget + $script:port = [int]$target.port + $health = $null + $origin = "http://$($target.host):$($script:port)" + try { $health = Read-JsonUrl "$origin/healthz" } catch { } + $pidMatches = $null -eq $target.pid -or [int]$target.pid -eq [int]$health.pid + $script:online = $null -ne $health -and $health.status -eq "ok" -and $health.service -eq "opencodex" -and [int]$health.port -eq $script:port -and $pidMatches + $script:proxyPid = if ($script:online) { [int]$health.pid } else { $null } + if ($script:online) { + $statusItem.Text = "Proxy: Online (port $($script:port))" + $notify.Text = "opencodex: Online" + $startItem.Enabled = $false + $stopItem.Enabled = $true + $restartItem.Enabled = $true + try { + $startup = Read-JsonUrl "$origin/api/startup-health" + $label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" } + $safetyItem.Text = "Restart safety: $label" + $notify.Icon = if ($startup.status -eq "at-risk") { [System.Drawing.SystemIcons]::Warning } else { [System.Drawing.SystemIcons]::Information } + } catch { + $safetyItem.Text = "Restart safety: unavailable" + $notify.Icon = [System.Drawing.SystemIcons]::Information + } + } else { + $statusItem.Text = "Proxy: Offline" + $safetyItem.Text = "Restart safety: start the proxy to inspect" + $notify.Text = "opencodex: Offline" + $notify.Icon = [System.Drawing.SystemIcons]::Error + $startItem.Enabled = $true + $stopItem.Enabled = $false + $restartItem.Enabled = $false + } + $heartbeat = @{ pid = $PID; timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() } + if ($HostPid -gt 0) { $heartbeat.hostPid = $HostPid } + $heartbeatJson = $heartbeat | ConvertTo-Json -Compress + [System.IO.File]::WriteAllText($heartbeatPath, $heartbeatJson, (New-Object System.Text.UTF8Encoding($false))) + + if ($null -ne $script:pendingAction) { + $now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + $elapsed = $now - $script:pendingStarted + $reached = ($script:pendingAction -eq "Start Proxy" -and $script:online) -or + ($script:pendingAction -eq "Stop Proxy" -and -not $script:online) -or + ($script:pendingAction -eq "Restart Proxy" -and $elapsed -gt 3000 -and $script:online -and $script:proxyPid -ne $script:pendingOldProxyPid) + if ($reached) { Complete-PendingAction $true } + elseif ($now -gt $script:pendingDeadline) { Complete-PendingAction $false } + } +} + +$openItem.add_Click({ Start-OcxCommand @("gui") }) +$startItem.add_Click({ + $statusItem.Text = "Proxy: Starting..." + Set-PendingAction "Start Proxy" 15 + if (-not (Start-OcxCommand @("__tray-start"))) { $script:pendingAction = $null } +}) +$stopItem.add_Click({ + $statusItem.Text = "Proxy: Stopping..." + Set-PendingAction "Stop Proxy" 15 + if (-not (Start-OcxCommand @("stop"))) { $script:pendingAction = $null } +}) +$restartItem.add_Click({ + $statusItem.Text = "Proxy: Restarting..." + Set-PendingAction "Restart Proxy" 20 + if (-not (Start-OcxCommand @("__tray-restart"))) { $script:pendingAction = $null } +}) +$logsItem.add_Click({ + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $OpenCodexHome + $psi.UseShellExecute = $true + [void][System.Diagnostics.Process]::Start($psi) +}) +$exitItem.add_Click({ [System.Windows.Forms.Application]::Exit() }) +$notify.add_DoubleClick({ Start-OcxCommand @("gui") }) +$menu.add_Opening({ Update-TrayState }) + +$timer = New-Object System.Windows.Forms.Timer +$timer.Interval = 3000 +$timer.add_Tick({ + if ($stopEvent.WaitOne(0)) { + [System.Windows.Forms.Application]::Exit() + return + } + Update-TrayState +}) +$notify.ContextMenuStrip = $menu +$notify.Visible = $true +$notify.Text = "opencodex: Checking..." + +try { + Update-TrayState + $timer.Start() + [System.Windows.Forms.Application]::Run() +} finally { + $timer.Stop() + $timer.Dispose() + $notify.Visible = $false + $notify.Dispose() + $menu.Dispose() + try { Remove-Item -LiteralPath $heartbeatPath -Force -ErrorAction SilentlyContinue } catch { } + try { $mutex.ReleaseMutex() } catch { } + $mutex.Dispose() + $stopEvent.Dispose() +} diff --git a/src/tray/windows.ts b/src/tray/windows.ts new file mode 100644 index 00000000000..b4791377ca5 --- /dev/null +++ b/src/tray/windows.ts @@ -0,0 +1,507 @@ +import { execFile, execFileSync, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { expandUserPath, getConfigDir } from "../config"; +import { durableBunPath } from "../lib/bun-runtime"; +import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl"; + +const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; +const TRAY_STATE_VERSION = 1; +const FOREIGN_RUN_VALUE = ""; + +export interface WindowsTrayEntry { + bun: string; + cli: string; + script: string; + codexHome: string; + opencodexHome: string; +} + +interface WindowsTrayState extends WindowsTrayEntry { + version: 1; + launcherPath?: string; + runValue: string; + runCommand: string; +} + +export interface WindowsTrayStatus { + supported: boolean; + installed: boolean; + running: boolean; + stale: boolean; + summary: string; +} + +function trayStatePath(): string { + return join(getConfigDir(), "tray-state.json"); +} + +function trayHeartbeatPath(): string { + return join(getConfigDir(), "tray-heartbeat.json"); +} + +function installedTrayScriptPath(): string { + return join(getConfigDir(), "opencodex-tray.ps1"); +} + +export function windowsTrayStatePathsOwned( + state: Pick & { launcherPath?: string }, + configDir = getConfigDir(), +): boolean { + if (resolve(state.opencodexHome) !== resolve(configDir)) return false; + if (resolve(state.script) !== resolve(join(configDir, "opencodex-tray.ps1"))) return false; + return state.launcherPath === undefined + || resolve(state.launcherPath) === resolve(join(configDir, "opencodex-tray.vbs")); +} + +function sourceTrayScriptPath(): string { + return join(import.meta.dir, "windows-tray.ps1"); +} + +function currentCodexHome(): string { + const raw = process.env.CODEX_HOME?.trim(); + return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex"); +} + +function currentEntry(): WindowsTrayEntry { + return { + bun: durableBunPath(), + cli: join(import.meta.dir, "..", "cli", "index.ts"), + script: installedTrayScriptPath(), + codexHome: currentCodexHome(), + opencodexHome: getConfigDir(), + }; +} + +export function windowsTrayRunValue(opencodexHome: string): string { + const normalized = resolve(opencodexHome).replace(/[\\/]+$/, "").toLowerCase(); + return `OpenCodexTray-${createHash("sha256").update(normalized).digest("hex").slice(0, 12)}`; +} + +export function windowsPowerShellPath(systemRoot = process.env.SystemRoot): string { + const candidate = join( + systemRoot ?? "C:\\Windows", + "System32", + "WindowsPowerShell", + "v1.0", + "powershell.exe", + ); + return existsSync(candidate) ? candidate : "powershell.exe"; +} + +function registryExe(): string { + const candidate = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "reg.exe"); + return existsSync(candidate) ? candidate : "reg.exe"; +} + +function runRegistry(args: string[]): string { + return execFileSync(registryExe(), args, { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }).trim(); +} + +function safePath(value: string): string { + if (/[\u0000-\u001f\"]/.test(value)) { + throw new Error("Windows tray paths cannot contain quotes or control characters."); + } + return value; +} + +export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "Stop" = "Run", hostPid?: number): string[] { + const args = [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-STA", + "-ExecutionPolicy", "Bypass", + "-WindowStyle", "Hidden", + "-File", safePath(entry.script), + "-BunPath", safePath(entry.bun), + "-CliPath", safePath(entry.cli), + "-CodexHome", safePath(entry.codexHome), + "-OpenCodexHome", safePath(entry.opencodexHome), + "-Mode", mode, + ]; + if (Number.isSafeInteger(hostPid) && (hostPid ?? 0) > 0) args.push("-HostPid", String(hostPid)); + return args; +} + +function quoteRunValue(value: string): string { + safePath(value); + return `\"${value}\"`; +} + +/** Command persisted under HKCU Run. Every value is an owned absolute package/home path. */ +export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string { + return [ + quoteRunValue(powershell), + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-STA", + "-ExecutionPolicy", "Bypass", + "-WindowStyle", "Hidden", + "-File", quoteRunValue(entry.script), + "-BunPath", quoteRunValue(entry.bun), + "-CliPath", quoteRunValue(entry.cli), + "-CodexHome", quoteRunValue(entry.codexHome), + "-OpenCodexHome", quoteRunValue(entry.opencodexHome), + "-Mode", "Run", + ].join(" "); +} + +function readState(): WindowsTrayState | null { + try { + const state = JSON.parse(readFileSync(trayStatePath(), "utf8")) as Partial; + if (state.version !== TRAY_STATE_VERSION) return null; + for (const key of ["bun", "cli", "script", "codexHome", "opencodexHome", "runValue", "runCommand"] as const) { + if (typeof state[key] !== "string" || state[key].length === 0) return null; + } + if (state.launcherPath !== undefined && typeof state.launcherPath !== "string") return null; + const valid = state as WindowsTrayState; + for (const value of [valid.bun, valid.cli, valid.script, valid.codexHome, valid.opencodexHome]) safePath(value); + // State is advisory, not an authority for executable or deletion paths. In + // particular, never let a forged state file redirect PowerShell -File. + if (!windowsTrayStatePathsOwned(valid)) return null; + if (valid.runValue !== windowsTrayRunValue(valid.opencodexHome)) return null; + return valid; + } catch { + return null; + } +} + +function replaceOwnedFile(path: string, contents: string | Buffer): void { + const temporary = `${path}.${process.pid}.${Date.now()}.tmp`; + writeFileSync(temporary, contents, { mode: 0o600 }); + try { + try { chmodSync(temporary, 0o600); } catch { /* best-effort */ } + if (process.platform === "win32") { + const hardened = hardenSecretPath(temporary, { required: true }); + if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state."); + } + renameSync(temporary, path); + } finally { + try { if (existsSync(temporary)) unlinkSync(temporary); } catch { /* best-effort */ } + } +} + +function writeState(entry: WindowsTrayEntry, runValue: string, runCommand: string): void { + const path = trayStatePath(); + replaceOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n"); +} + +export function parseWindowsTrayRunValue(output: string, runValue: string): string | null { + const escaped = runValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`^\\s*${escaped}\\s+(\\S+)\\s*(.*)$`, "mi").exec(output); + if (!match || match[1] !== "REG_SZ" || !match[2]?.trim()) return FOREIGN_RUN_VALUE; + return match[2].trim(); +} + +function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): string | null { + try { + const output = runRegistry(["query", RUN_KEY, "/v", runValue, "/reg:64"]); + return parseWindowsTrayRunValue(output, runValue); + } catch (error) { + if ((error as { status?: unknown })?.status === 1) return null; + throw new Error("Unable to verify the owned Windows tray registry value; refusing to change persistence."); + } +} + +function readHeartbeat(): { pid: number; hostPid?: number; timestamp: number } | null { + try { + const heartbeat = JSON.parse(readFileSync(trayHeartbeatPath(), "utf8").replace(/^\uFEFF/, "")) as { pid?: unknown; hostPid?: unknown; timestamp?: unknown }; + if (!Number.isSafeInteger(heartbeat.pid) || (heartbeat.pid as number) <= 0 || typeof heartbeat.timestamp !== "number") return null; + const hostPid = Number.isSafeInteger(heartbeat.hostPid) && (heartbeat.hostPid as number) > 0 ? heartbeat.hostPid as number : undefined; + return { pid: heartbeat.pid as number, hostPid, timestamp: heartbeat.timestamp }; + } catch { + return null; + } +} + +function processAlive(pid: number): boolean { + try { process.kill(pid, 0); return true; } catch { return false; } +} + +function heartbeatProcessAlive(heartbeat = readHeartbeat()): boolean { + return Boolean(heartbeat && processAlive(heartbeat.pid)); +} + +function heartbeatRunning(): boolean { + const heartbeat = readHeartbeat(); + return Boolean(heartbeat && Date.now() - heartbeat.timestamp <= 15_000 && heartbeatProcessAlive(heartbeat)); +} + +function waitForHeartbeat(expected: boolean, timeoutMs = 8_000): boolean { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (heartbeatRunning() === expected) return true; + Bun.sleepSync(100); + } + return heartbeatRunning() === expected; +} + +function waitForTrayExit(previous: ReturnType, timeoutMs = 15_000): boolean { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const powershellExited = !previous || !processAlive(previous.pid); + const hostExited = !previous?.hostPid || !processAlive(previous.hostPid); + if (!heartbeatProcessAlive() && powershellExited && hostExited) return true; + Bun.sleepSync(100); + } + const powershellExited = !previous || !processAlive(previous.pid); + const hostExited = !previous?.hostPid || !processAlive(previous.hostPid); + return !heartbeatProcessAlive() && powershellExited && hostExited; +} + +function trayStatusFrom(registered: string | null): WindowsTrayStatus { + const state = readState(); + const heartbeat = readHeartbeat(); + const running = heartbeatProcessAlive(heartbeat); + const stale = registered !== null && (!state + || registered !== state.runCommand + || [state.bun, state.cli, state.script].some(path => !existsSync(path))) + || (running && (!heartbeat || Date.now() - heartbeat.timestamp > 15_000)); + const installed = registered !== null && state !== null && registered === state.runCommand && !stale; + const summary = registered === null + ? running ? "unregistered tray process is still running" : "not installed" + : stale + ? "startup registration is foreign, stale, or points to missing package files" + : running + ? "installed and running" + : "installed, not currently running"; + return { supported: true, installed, running, stale, summary }; +} + +export function getWindowsTrayStatus(): WindowsTrayStatus { + if (process.platform !== "win32") { + return { supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` }; + } + return trayStatusFrom(readOwnedRunValue()); +} + +export async function getWindowsTrayStatusAsync(): Promise { + if (process.platform !== "win32") { + return { supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` }; + } + const runValue = windowsTrayRunValue(getConfigDir()); + const registered = await new Promise((resolvePromise, rejectPromise) => { + execFile(registryExe(), ["query", RUN_KEY, "/v", runValue, "/reg:64"], { + encoding: "utf8", + timeout: 2_000, + windowsHide: true, + maxBuffer: 64 * 1024, + }, (error, stdout) => { + if (!error) return resolvePromise(parseWindowsTrayRunValue(stdout, runValue)); + if ((error as { code?: unknown }).code === 1) return resolvePromise(null); + rejectPromise(new Error("Unable to verify Windows tray registry status.")); + }); + }); + return trayStatusFrom(registered); +} + +function assertWindows(): void { + if (process.platform !== "win32") throw new Error(`The opencodex tray is Windows-only (current platform: ${process.platform}).`); +} + +function spawnTray(state: WindowsTrayEntry): void { + const child = spawn(state.bun, [state.cli, "__tray-host"], { + detached: true, + stdio: "ignore", + windowsHide: true, + env: { + ...process.env, + OCX_TRAY_ENTRY_B64: Buffer.from(JSON.stringify(state), "utf8").toString("base64"), + }, + }); + child.unref(); +} + +function parseTrayHostEntry(): WindowsTrayEntry { + const encoded = process.env.OCX_TRAY_ENTRY_B64; + if (!encoded) throw new Error("Missing tray host entry."); + const value = JSON.parse(Buffer.from(encoded, "base64").toString("utf8")) as Partial; + for (const key of ["bun", "cli", "script", "codexHome", "opencodexHome"] as const) { + if (typeof value[key] !== "string") throw new Error(`Invalid tray host field: ${key}`); + safePath(value[key]); + } + return value as WindowsTrayEntry; +} + +/** Detached Bun host keeps the attached WinForms PowerShell process alive. */ +export async function runWindowsTrayHost(): Promise { + assertWindows(); + const entry = parseTrayHostEntry(); + delete process.env.OCX_TRAY_ENTRY_B64; + const child = spawn(windowsPowerShellPath(), windowsTrayProcessArgs(entry, "Run", process.pid), { + stdio: "ignore", + windowsHide: true, + env: process.env, + }); + await new Promise((resolvePromise, rejectPromise) => { + child.once("error", rejectPromise); + child.once("exit", code => { + if (code && code !== 0) rejectPromise(new Error(`Windows tray host exited with code ${code}.`)); + else resolvePromise(); + }); + }); +} + +function signalTrayStop(): ReturnType { + const previous = readHeartbeat(); + // The stop event name depends only on the current home. Never execute paths + // recovered from tray-state.json while attempting cleanup or repair. + execFileSync(windowsPowerShellPath(), windowsTrayProcessArgs(currentEntry(), "Stop"), { + stdio: "ignore", + windowsHide: true, + timeout: 15_000, + }); + return previous; +} + +export function installWindowsTray(startNow = true): WindowsTrayStatus { + assertWindows(); + const entry = currentEntry(); + const sourceScript = sourceTrayScriptPath(); + for (const path of [entry.bun, entry.cli, sourceScript]) { + if (!existsSync(path)) throw new Error(`Cannot install the tray because a required file is missing: ${path}`); + } + if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 }); + const runCommand = buildWindowsTrayRunCommand(entry); + const runValue = windowsTrayRunValue(entry.opencodexHome); + const existing = readOwnedRunValue(runValue); + const state = readState(); + if (existing && (!state || existing !== state.runCommand)) { + throw new Error(`Refusing to replace a foreign or unowned HKCU Run value named ${runValue}.`); + } + if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) { + throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`); + } + const wasRunning = heartbeatProcessAlive(); + if (wasRunning && !state) { + throw new Error("Refusing to replace an unowned running tray process. Exit it before installing."); + } + if (wasRunning && state) { + const previous = signalTrayStop(); + if (!waitForTrayExit(previous)) throw new Error("The old tray did not exit; refusing to replace its persistent script."); + } + + const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null; + const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null; + const restorePreviousInstall = () => { + try { + if (previousScriptBytes) replaceOwnedFile(entry.script, previousScriptBytes); + else if (existsSync(entry.script)) unlinkSync(entry.script); + } catch { /* rollback best-effort */ } + try { + if (previousStateBytes) replaceOwnedFile(trayStatePath(), previousStateBytes); + else if (existsSync(trayStatePath())) unlinkSync(trayStatePath()); + } catch { /* rollback best-effort */ } + try { + if (existing !== null) runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", existing, "/f", "/reg:64"]); + else runRegistry(["delete", RUN_KEY, "/v", runValue, "/f", "/reg:64"]); + } catch { /* rollback best-effort */ } + if (wasRunning && state && !heartbeatRunning()) { + try { + spawnTray(currentEntry()); + waitForHeartbeat(true); + } catch { /* retain the primary installation failure */ } + } + }; + + try { + const hardenedDir = hardenSecretDir(getConfigDir(), { required: true }); + if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence."); + replaceOwnedFile(entry.script, readFileSync(sourceScript)); + runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]); + writeState(entry, runValue, runCommand); + } catch (error) { + restorePreviousInstall(); + throw error; + } + if (startNow && !heartbeatRunning()) spawnTray(entry); + if (startNow && !waitForHeartbeat(true)) { + restorePreviousInstall(); + throw new Error("The tray startup registration was installed, but the tray process did not become healthy."); + } + if (state?.launcherPath && existsSync(state.launcherPath)) { + try { unlinkSync(state.launcherPath); } catch { /* old owned VBS is inert after a committed Run replacement */ } + } + return getWindowsTrayStatus(); +} + +export function startWindowsTray(): WindowsTrayStatus { + assertWindows(); + const state = readState(); + if (!state || readOwnedRunValue(state.runValue) !== state.runCommand) throw new Error("The tray is not installed. Install it first."); + // Persisted state proves registration ownership but never selects an + // executable. Resolve every launch path from the running installation. + if (!heartbeatRunning()) spawnTray(currentEntry()); + if (!waitForHeartbeat(true)) throw new Error("The tray process did not become healthy after launch."); + return getWindowsTrayStatus(); +} + +export function stopWindowsTray(): WindowsTrayStatus { + assertWindows(); + const state = readState(); + let previous = readHeartbeat(); + if (state) { + previous = signalTrayStop(); + } + if (!waitForTrayExit(previous)) throw new Error("The tray did not exit after the stop signal. Its login registration was preserved."); + return getWindowsTrayStatus(); +} + +export function uninstallWindowsTray(): WindowsTrayStatus { + assertWindows(); + const state = readState(); + const existing = state ? readOwnedRunValue(state.runValue) : readOwnedRunValue(); + if (existing && (!state || existing !== state.runCommand)) { + throw new Error(`Refusing to remove a foreign or unowned HKCU Run value named ${state?.runValue ?? windowsTrayRunValue(getConfigDir())}.`); + } + let previous = readHeartbeat(); + if (state) { + previous = signalTrayStop(); + } + if (!waitForTrayExit(previous)) throw new Error("The tray did not exit; refusing to remove its owned registration or state."); + if (existing) runRegistry(["delete", RUN_KEY, "/v", state?.runValue ?? windowsTrayRunValue(getConfigDir()), "/f", "/reg:64"]); + const ownedPaths = [trayStatePath(), trayHeartbeatPath(), ...(state?.launcherPath ? [state.launcherPath] : [])]; + if (state?.script && resolve(state.script) === resolve(installedTrayScriptPath())) ownedPaths.push(state.script); + for (const path of ownedPaths) { + try { if (existsSync(path)) unlinkSync(path); } catch { /* best-effort */ } + } + return getWindowsTrayStatus(); +} + +/** Update hook: refresh trusted paths and relaunch only when the tray was already installed. */ +export function repairWindowsTrayIfInstalled(startNow = true): WindowsTrayStatus | null { + if (process.platform !== "win32" || !readState()) return null; + return installWindowsTray(startNow); +} + +export async function windowsTrayCommand(args: string[]): Promise { + const wantsJson = args.includes("--json"); + const startNow = !args.includes("--no-start"); + const values = args.filter(value => value !== "--json" && value !== "--no-start"); + const sub = values[0] ?? "status"; + if (args.includes("--no-start") && sub !== "install" || values.length > 1 || !["install", "start", "stop", "status", "uninstall", "remove"].includes(sub)) { + console.error("Usage: ocx tray [--json] [--no-start]"); + process.exitCode = 1; + return; + } + try { + const status = sub === "install" ? installWindowsTray(startNow) + : sub === "start" ? startWindowsTray() + : sub === "stop" ? stopWindowsTray() + : sub === "uninstall" || sub === "remove" ? uninstallWindowsTray() + : getWindowsTrayStatus(); + console.log(wantsJson ? JSON.stringify(status) : `Windows tray: ${status.summary}`); + } catch (error) { + if (wantsJson) console.log(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })); + else console.error(`Windows tray error: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } +} diff --git a/src/update/index.ts b/src/update/index.ts index cee5cdfc7c5..935a43221f4 100644 --- a/src/update/index.ts +++ b/src/update/index.ts @@ -170,6 +170,29 @@ export async function runUpdate(): Promise { const { isServiceInstalled } = await import("../service"); serviceWasInstalled = isServiceInstalled(); } catch { /* best-effort */ } + let trayWasInstalled = false; + let trayWasRunning = false; + if (process.platform === "win32") { + try { + const { getWindowsTrayStatus, stopWindowsTray } = await import("../tray/windows"); + const tray = getWindowsTrayStatus(); + trayWasInstalled = tray.installed; + trayWasRunning = tray.running; + if (tray.running) { + const stopped = stopWindowsTray(); + if (stopped.running) throw new Error("the tray still reports running after shutdown"); + } + } catch (error) { + if (trayWasRunning) { + try { + const { startWindowsTray } = await import("../tray/windows"); + startWindowsTray(); + } catch { /* preserve the handoff failure */ } + } + console.error(`⚠️ Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + } // Capture listen target before stop clears runtime state (same contract as GUI update worker). // Prefer a live runtime record; a stale crashed leftover must not override config.port. @@ -200,6 +223,12 @@ export async function runUpdate(): Promise { }); if (stopStdio === "pipe") logSpawnOutput("", stop); if (stop.status !== 0 || readPid() || readRuntimePort()) { + if (trayWasRunning) { + try { + const { startWindowsTray } = await import("../tray/windows"); + startWindowsTray(); + } catch { /* preserve the proxy stop failure */ } + } console.error("⚠️ Could not stop the running proxy; aborting the update. Run 'ocx stop' and retry."); process.exit(1); } @@ -238,6 +267,16 @@ export async function runUpdate(): Promise { } catch (e) { console.warn(`⚠️ Shim repair skipped: ${e instanceof Error ? e.message : e}`); } + if (trayWasInstalled) { + const trayArgs = [process.argv[1], "tray", "install", ...(!trayWasRunning ? ["--no-start"] : [])]; + const tray = spawnSync(process.execPath, trayArgs, { stdio: "inherit", windowsHide: true }); + if (tray.status === 0) { + console.log("🔧 Refreshed Windows tray startup paths."); + } else { + console.warn("⚠️ Windows tray refresh failed. Run 'ocx tray install'."); + if (trayWasRunning) spawnSync(process.execPath, [process.argv[1], "tray", "start"], { stdio: "ignore", windowsHide: true }); + } + } // The stop above unloaded any managed service; reinstall it with the NEW files // (spawn the fresh cli.ts so updated code writes the baked paths) so a // launchd/schtasks/systemd user isn't left with the background proxy down. @@ -286,6 +325,12 @@ export async function runUpdate(): Promise { console.log("Restart the proxy: ocx start"); } } else { + if (trayWasRunning) { + try { + const { startWindowsTray } = await import("../tray/windows"); + startWindowsTray(); + } catch { /* keep the primary update failure */ } + } console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`); process.exit(1); } diff --git a/src/update/job.ts b/src/update/job.ts index 6c6789e4605..00352e394d5 100644 --- a/src/update/job.ts +++ b/src/update/job.ts @@ -474,6 +474,8 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar port: runtimeTrusted ? rt.port : configPort, hostname: (runtimeTrusted ? rt.hostname : undefined) ?? preUpdateConfig.hostname ?? "127.0.0.1", }; + let trayWasInstalled = false; + let trayWasRunning = false; if (!job) { job = { id: jobId, @@ -517,6 +519,25 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar command: cmd.display, }, integrityLine); + if (process.platform === "win32") { + try { + const { getWindowsTrayStatus, stopWindowsTray } = await import("../tray/windows"); + const tray = getWindowsTrayStatus(); + trayWasInstalled = tray.installed; + trayWasRunning = tray.running; + if (tray.running) { + const stopped = stopWindowsTray(); + if (stopped.running) throw new Error("the tray still reports running after shutdown"); + } + } catch (error) { + updateJob(job, { + status: "failed", + error: `Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`, + }); + return; + } + } + /* [Decision Log] - 목적: GUI 요청 처리 프로세스가 자신이 실행 중인 패키지를 직접 덮어쓰지 않도록 업데이트를 별도 worker에서 수행한다. - 대안 분석: (1) 서버에서 runUpdate 직접 호출: process.exit/stdio/실행 파일 교체 위험. (2) GUI에서 CLI 명령 안내만 제공: 자동 업데이트 UX 부족. (3) 숨은 worker가 Node launcher/Bun 전역 명령을 실행: 상태 추적과 안전한 재시작이 가능. @@ -524,6 +545,12 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar */ const result = runLoggedCommand(job, cmd.bin, cmd.args, UPDATE_TIMEOUT_MS); if (result.status !== 0) { + if (trayWasRunning) { + try { + const { startWindowsTray } = await import("../tray/windows"); + startWindowsTray(); + } catch { /* retain the primary update failure */ } + } updateJob(job, { status: "failed", exitCode: result.status, @@ -533,6 +560,15 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar return; } + if (trayWasInstalled) { + const trayArgs = [process.argv[1], "tray", "install", ...(!trayWasRunning ? ["--no-start"] : [])]; + const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000); + if (tray.status !== 0) { + updateJob(job, {}, "Windows tray refresh failed; run 'ocx tray install'."); + if (trayWasRunning) runLoggedCommand(job, process.execPath, [process.argv[1], "tray", "start"], 15_000); + } + } + if (restart) { job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy..."); await restartAfterUpdate(job, captured); @@ -543,6 +579,12 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar updateJob(job, { status: "succeeded", restarted: false }, "Update installed. Restart the proxy to use the new version."); } catch (err) { + if (trayWasRunning) { + try { + const { startWindowsTray } = await import("../tray/windows"); + startWindowsTray(); + } catch { /* retain the primary worker failure */ } + } updateJob(job, { status: "failed", error: err instanceof Error ? err.message : String(err), diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index 8a47781729d..52ecefe490b 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -14,6 +14,7 @@ starts the proxy when needed and opens `http://localhost:`. | --- | --- | | Config/settings | Read safe config/settings views; mutate supported settings only. Full `PUT /api/config` is disabled so masked secrets are not round-tripped. | | Startup safety | `GET /api/startup-health` reports whether injected Codex routing is restart-safe, with secret-free service/shim diagnostics. On Windows a healthy script shim is CLI-only; Codex Desktop requires the background service for full protection. | +| Windows tray | `GET/POST /api/windows-tray` controls an owned, per-user HKCU login tray. The tray delegates fixed actions to the CLI and is never a proxy supervisor or restart-protection signal. | | Providers | Create/update/delete ordinary provider configs and enrich registry metadata. The reserved `openai` card exposes Pool(default)/Direct account mode; `openai-apikey` remains the separate API route. | | Models | Fetch routed model lists, disabled model visibility, and catalog-facing ids. | | OAuth | Login/status/logout for OAuth-backed providers, plus multiauth account management: `GET /api/oauth/accounts`, `PUT /api/oauth/accounts/active`, `PUT /api/oauth/accounts/alias`, `DELETE /api/oauth/accounts` list masked accounts per provider, switch the active one, edit its display-only alias, and remove one. Login accepts `addAccount: true` to force a fresh browser identity. Device flows return a structured `deviceCode`; the GUI highlights and copies it before the user opens the verification page. | @@ -49,9 +50,12 @@ endpoint restores native Codex config, stops any installed service to prevent re The dashboard sidebar exposes a **Startup safety** page. Its warning state is derived from active Codex routing plus the actual service and launcher-shim installation state; the -`codexAutoStart` preference alone is never presented as proof of restart protection. The page is -read-only and shows copyable repair commands (`ocx service install`, `ocx codex-shim install`, and -`ocx restore`) instead of installing lifecycle hooks from the proxy process. +`codexAutoStart` preference alone is never presented as proof of restart protection. The page shows +copyable repair commands (`ocx service install`, `ocx codex-shim install`, and `ocx restore`). On +Windows it can also install an owned, per-user system tray. The resident tray owns only its icon, +home-scoped singleton, and HKCU Run registration; fixed proxy actions delegate to the CLI so drain, +service conflict handling, native restore, and PID identity remain centralized. Tray presence never +makes `startup.status` protected. ## UX boundary diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index 9246410a388..d03967a851c 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -1,13 +1,19 @@ import { describe, expect, test } from "bun:test"; import { deriveStartupHealth, startupHealthSummary } from "../src/codex/autostart-health"; -import { hasInjectedCodexRouting } from "../src/codex/inject"; +import { classifyCodexRouting, hasInjectedCodexRouting } from "../src/codex/inject"; import { handleManagementAPI } from "../src/server/management-api"; +import { invalidateStartupHealthCache, markStartupHealthDiagnosticStale } from "../src/server/startup-health-cache"; import type { OcxConfig } from "../src/types"; const base = { - routingInjected: true, + routingKind: "opencodex-local" as const, autostartEnabled: true, serviceInstalled: false, + serviceViable: false, + serviceEnabled: false, + serviceRunning: false, + serviceStale: false, + serviceConflict: false, serviceSupported: true, shimInstalled: false, shimHealthy: false, @@ -27,7 +33,7 @@ describe("Codex startup health", () => { }); test("treats a background service as restart protection", () => { - const health = deriveStartupHealth({ ...base, serviceInstalled: true }); + const health = deriveStartupHealth({ ...base, serviceInstalled: true, serviceViable: true, serviceEnabled: true, serviceRunning: true }); expect(health).toMatchObject({ status: "protected", rebootSafe: true, @@ -36,17 +42,27 @@ describe("Codex startup health", () => { }); }); + test("never preserves a green local-routing claim when diagnostics are stale", () => { + const protectedHealth = deriveStartupHealth({ ...base, serviceInstalled: true, serviceViable: true, serviceEnabled: true, serviceRunning: true }); + expect(markStartupHealthDiagnosticStale(protectedHealth)).toMatchObject({ + status: "at-risk", + rebootSafe: false, + protection: "none", + diagnosticStale: true, + }); + }); + test("classifies a healthy Windows shim as CLI-only rather than Desktop-safe", () => { const windowsShim = deriveStartupHealth({ ...base, shimInstalled: true, shimHealthy: true }); expect(windowsShim).toMatchObject({ protection: "shim", shimCoverage: "cli-only", status: "at-risk" }); const unixShim = deriveStartupHealth({ ...base, platform: "linux", shimInstalled: true, shimHealthy: true }); - expect(unixShim).toMatchObject({ protection: "shim", shimCoverage: "full", status: "protected" }); + expect(unixShim).toMatchObject({ protection: "shim", shimCoverage: "cli-only", status: "at-risk" }); expect(deriveStartupHealth({ ...base, shimInstalled: true, shimHealthy: false }).status).toBe("at-risk"); expect(deriveStartupHealth({ ...base, autostartEnabled: false, shimInstalled: true, shimHealthy: true }).status).toBe("at-risk"); }); test("native routing has no opencodex restart dependency", () => { - const health = deriveStartupHealth({ ...base, routingInjected: false }); + const health = deriveStartupHealth({ ...base, routingKind: "native" }); expect(health).toMatchObject({ status: "native", rebootSafe: true, protection: "none" }); }); @@ -62,15 +78,64 @@ describe("Codex startup health", () => { 'base_url = "http://127.0.0.1:10100/v1"', ].join("\n"))).toBe(true); expect(hasInjectedCodexRouting('openai_base_url = "http://127.0.0.1:10100/v1"')).toBe(false); + expect(classifyCodexRouting('openai_base_url = "http://127.0.0.1:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "https://gateway.example/v1"')).toBe("custom-remote"); + expect(classifyCodexRouting([ + "[features]", + 'model_provider = "opencodex"', + "[model_providers.opencodex]", + 'base_url = "http://127.0.0.1:10100/v1"', + ].join("\n"))).toBe("native"); + expect(classifyCodexRouting([ + 'model_provider = "opencodex"', + "[model_providers.opencodex]", + 'base_url = "https://gateway.example/v1"', + ].join("\n"))).toBe("opencodex-local"); + expect(classifyCodexRouting([ + "# Auto-injected by opencodex", + 'openai_base_url = "http://192.168.1.10:10100/v1"', + ].join("\n"))).toBe("opencodex-local"); + }); + + test("fails closed for installed-but-broken services and custom local gateways", () => { + expect(deriveStartupHealth({ ...base, serviceInstalled: true, serviceStale: true })).toMatchObject({ + status: "at-risk", + rebootSafe: false, + serviceViable: false, + }); + expect(deriveStartupHealth({ ...base, routingKind: "custom-local" })).toMatchObject({ + status: "at-risk", + routingInjected: false, + localRoutingDependency: true, + protection: "none", + recommendedCommand: "ocx restore", + }); + expect(deriveStartupHealth({ ...base, routingKind: "custom-local", serviceInstalled: true, serviceViable: true, serviceEnabled: true, serviceRunning: true })).toMatchObject({ + status: "at-risk", + rebootSafe: false, + protection: "none", + recommendedCommand: "ocx restore", + }); + expect(deriveStartupHealth({ ...base, routingKind: "custom-remote" })).toMatchObject({ + status: "native", + localRoutingDependency: false, + }); }); test("exposes a secret-free startup health DTO to the dashboard", async () => { + invalidateStartupHealthCache(); const url = new URL("http://localhost/api/startup-health"); - const response = await handleManagementAPI( + let timerFired = false; + const timer = setTimeout(() => { timerFired = true; }, 25); + const responsePromise = handleManagementAPI( new Request(url), url, { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig, ); + await Bun.sleep(75); + expect(timerFired).toBe(true); // service-manager probes run in a child, not the proxy event loop + clearTimeout(timer); + const response = await responsePromise; expect(response?.status).toBe(200); const body = await response!.json() as Record; diff --git a/tests/service.test.ts b/tests/service.test.ts index 50f06ea96b4..34d29a030b5 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { saveConfig } from "../src/config"; import { windowsEnvIndirectBatchValue } from "../src/lib/win-paths"; -import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsServiceScript, buildWindowsTaskXml, normalizeServiceSubcommand, resolveServiceListenPort, serviceLogPath, serviceStatusSummary } from "../src/service"; +import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsServiceScript, buildWindowsTaskXml, deriveWindowsServiceDiagnostic, normalizeServiceSubcommand, resolveServiceListenPort, serviceLogPath, serviceStartableFromTray, serviceStatusSummary, windowsTaskRegistrationHealthy } from "../src/service"; import { serviceApiTokenFilePath } from "../src/lib/service-secrets"; import type { OcxConfig } from "../src/types"; @@ -237,6 +237,21 @@ describe("Windows service task", () => { expect(xml).not.toContain("C:\\Users\\a&b\\.opencodex\\opencodex-service.cmd"); }); + test("validates the registered scheduler action, trigger, principal, and settings", () => { + const wscript = "C:\\Windows\\System32\\wscript.exe"; + const launcher = "C:\\Users\\Test\\.opencodex\\service-launcher.vbs"; + const xml = buildWindowsTaskXml("ignored.cmd", launcher).replace(/.*?<\/Command>/, `${wscript}`); + expect(windowsTaskRegistrationHealthy(xml, wscript, launcher)).toBe(true); + for (const mutated of [ + xml.replace("", ""), + xml.replace("InteractiveToken", "Password"), + xml.replace("LeastPrivilege", "HighestAvailable"), + xml.replace("IgnoreNew", "Parallel"), + xml.replace(wscript, "C:\\Windows\\System32\\cmd.exe"), + xml.replace(launcher, "C:\\Temp\\foreign.vbs"), + ]) expect(windowsTaskRegistrationHealthy(mutated, wscript, launcher)).toBe(false); + }); + test("hidden launcher VBS stays resident and escapes quotes in the wrapper path", () => { const vbs = buildWindowsLauncherVbs('C:\\Users\\quo"te\\.opencodex\\opencodex-service.cmd'); @@ -476,6 +491,35 @@ describe("service lifecycle cleanup ordering", () => { }); describe("service diagnostics", () => { + const base = { + schedulerInstalled: false, + schedulerEnabled: false, + schedulerAssetsHealthy: true, + nativeStatus: "nonexistent" as const, + staleBakedPaths: false, + nativeRepairAssetsOnly: false, + diagnostics: "logs: test", + }; + + test("fails closed for disabled, stale, conflicting, stopped, and ghost Windows services", () => { + expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true })).toMatchObject({ viable: true, backend: "scheduler" }); + expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true })).toMatchObject({ viable: false, enabled: false }); + expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, staleBakedPaths: true })).toMatchObject({ viable: false, stale: true }); + expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, nativeStatus: "started" })).toMatchObject({ viable: false, conflict: true }); + expect(deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped" })).toMatchObject({ installed: true, viable: false, running: false }); + expect(deriveWindowsServiceDiagnostic({ ...base, nativeRepairAssetsOnly: true })).toMatchObject({ installed: false, viable: false, stale: true }); + }); + + test("a stopped healthy WinSW service remains startable from the tray", () => { + const stoppedNative = deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped" }); + expect(serviceStartableFromTray(stoppedNative)).toBe(true); + expect(serviceStartableFromTray({ ...stoppedNative, stale: true })).toBe(false); + expect(serviceStartableFromTray({ ...stoppedNative, conflict: true })).toBe(false); + expect(serviceStartableFromTray(deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "unknown" }))).toBe(false); + const disabledScheduler = deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: false }); + expect(serviceStartableFromTray(disabledScheduler)).toBe(false); + }); + test("status summary exposes the service log path", () => { const summary = serviceStatusSummary(); diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts new file mode 100644 index 00000000000..be9d13cedf4 --- /dev/null +++ b/tests/windows-tray.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + buildWindowsTrayRunCommand, + parseWindowsTrayRunValue, + windowsTrayProcessArgs, + windowsTrayRunValue, + windowsTrayStatePathsOwned, + type WindowsTrayEntry, +} from "../src/tray/windows"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { OcxConfig } from "../src/types"; + +const entry: WindowsTrayEntry = { + bun: "C:\\사용자 공간\\%TEMP% ! ^ ( ) & 검증\\bun.exe", + cli: "C:\\사용자 공간\\%TEMP% ! ^ ( ) & 검증\\src\\cli\\index.ts", + script: "C:\\사용자 공간\\%TEMP% ! ^ ( ) & 검증\\src\\tray\\windows-tray.ps1", + codexHome: "C:\\사용자 공간\\.codex", + opencodexHome: "C:\\사용자 공간\\%TEMP% ! ^ ( ) & 검증\\.opencodex", +}; + +describe("Windows tray packaging and command safety", () => { + test("uses fixed argv for the hidden PowerShell host", () => { + const args = windowsTrayProcessArgs(entry); + expect(args).toContain("-NoProfile"); + expect(args).toContain("-NonInteractive"); + expect(args).toContain("-STA"); + expect(args).toContain(entry.script); + expect(args).toContain(entry.bun); + expect(args).toContain(entry.cli); + expect(args).not.toContain("-Command"); + expect(windowsTrayProcessArgs(entry, "Run", 4242)).toContain("4242"); + }); + + test("quotes metacharacter and Unicode paths without shell interpolation", () => { + const powershellCommand = buildWindowsTrayRunCommand(entry, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"); + expect(powershellCommand).toContain(`-File "${entry.script}"`); + expect(powershellCommand).toContain(`-OpenCodexHome "${entry.opencodexHome}"`); + expect(powershellCommand).not.toContain("cmd /c"); + expect(powershellCommand).not.toContain("-Command"); + }); + + test("rejects quote and control-character path injection", () => { + expect(() => windowsTrayProcessArgs({ ...entry, opencodexHome: 'C:\\bad" -Command whoami' })).toThrow(); + expect(() => windowsTrayProcessArgs({ ...entry, cli: "C:\\bad\r\nwhoami" })).toThrow(); + }); + + test("never trusts state-selected executable or deletion paths", () => { + const home = "C:\\Users\\Test\\.opencodex"; + expect(windowsTrayStatePathsOwned({ + opencodexHome: home, + script: join(home, "opencodex-tray.ps1"), + launcherPath: join(home, "opencodex-tray.vbs"), + }, home)).toBe(true); + expect(windowsTrayStatePathsOwned({ + opencodexHome: home, + script: "C:\\attacker\\payload.ps1", + }, home)).toBe(false); + expect(windowsTrayStatePathsOwned({ + opencodexHome: home, + script: join(home, "opencodex-tray.ps1"), + launcherPath: "C:\\victim\\document.txt", + }, home)).toBe(false); + }); + + test("normalizes equivalent homes to one owned Run value", () => { + expect(windowsTrayRunValue("C:\\Users\\Test\\.opencodex")) + .toBe(windowsTrayRunValue("C:\\Users\\Test\\.opencodex\\.")); + }); + + test("treats an unexpected registry type or unreadable value as foreign", () => { + const value = "OpenCodexTray-test"; + const command = '"C:\\Windows\\powershell.exe" -File "C:\\tray.ps1"'; + expect(parseWindowsTrayRunValue(` ${value} REG_SZ ${command}`, value)).toBe(command); + expect(parseWindowsTrayRunValue(` ${value} REG_EXPAND_SZ ${command}`, value)).not.toBe(command); + expect(parseWindowsTrayRunValue("unexpected output", value)).not.toBeNull(); + }); + + test("PowerShell controller uses mutex/event shutdown and bans command evaluation", () => { + const typescript = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows.ts"), "utf8"); + const source = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows-tray.ps1"), "utf8"); + expect(typescript).not.toContain("\u0000"); + expect(typescript).toContain("OCX_TRAY_ENTRY_B64"); + expect(typescript).toContain('detached: true'); + expect(source).toContain("System.Threading.Mutex"); + expect(source).toContain("System.Threading.EventWaitHandle"); + expect(source).toContain("GetFullPath"); + expect(source).toContain("GetPathRoot"); + expect(source).toContain("$heartbeat.hostPid = $HostPid"); + expect(source).toContain('Start-OcxCommand @("__tray-restart")'); + expect(source).not.toContain("Invoke-Expression"); + expect(source).not.toContain("taskkill"); + expect(source).not.toContain("Stop-Process"); + }); + + test("serves tray status without blocking the proxy event loop", async () => { + if (process.platform !== "win32") return; + const url = new URL("http://localhost/api/windows-tray"); + let timerFired = false; + const timer = setTimeout(() => { timerFired = true; }, 0); + const responsePromise = handleManagementAPI( + new Request(url), + url, + { port: 10100, providers: {}, defaultProvider: "openai" } as OcxConfig, + ); + await Bun.sleep(50); + expect(timerFired).toBe(true); + clearTimeout(timer); + const response = await responsePromise; + expect(response?.status).toBe(200); + const body = await response!.json() as Record; + expect(body.supported).toBe(true); + expect(typeof body.installed).toBe("boolean"); + expect(typeof body.running).toBe("boolean"); + }); + + test("copies the tray script into the hardened home and gates all update lanes", () => { + const root = join(import.meta.dir, ".."); + const tray = readFileSync(join(root, "src", "tray", "windows.ts"), "utf8"); + expect(tray).toContain('join(getConfigDir(), "opencodex-tray.ps1")'); + expect(tray).toContain("const hardened = hardenSecretPath(temporary, { required: true })"); + expect(tray).toContain("if (!hardened.ok)"); + expect(tray).toContain("if (!hardenedDir.ok)"); + expect(tray).toContain("refusing to replace its persistent script"); + expect(tray).toContain("restorePreviousInstall"); + expect(tray).toContain("previousStateBytes"); + expect(tray).toContain("previousScriptBytes"); + expect(tray).toContain('windowsTrayProcessArgs(currentEntry(), "Stop")'); + expect(tray).not.toContain("spawnTray(state)"); + + const updateSources = [ + join(root, "src", "update", "index.ts"), + join(root, "src", "update", "job.ts"), + join(root, "bin", "ocx.mjs"), + ].map(path => readFileSync(path, "utf8")); + for (const source of updateSources) { + expect(source).toContain("tray"); + expect(source).toContain("stop"); + expect(source).toContain("aborting before package replacement"); + } + }); +}); From d5432d5a9fab3758720f4181c6f81fadbf28cade Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:22:36 +0900 Subject: [PATCH 03/11] fix(windows): fail closed on stale lifecycle state --- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/Dashboard.tsx | 2 +- gui/src/pages/Startup.tsx | 5 +- src/cli/index.ts | 3 +- src/codex/autostart-health.ts | 7 ++- src/codex/inject.ts | 44 +++++++++++----- src/server/startup-health-cache.ts | 4 +- src/service.ts | 4 ++ src/tray/windows.ts | 80 ++++++++++++++++++++++-------- tests/autostart-health.test.ts | 15 ++++++ tests/service.test.ts | 5 +- tests/windows-tray.test.ts | 17 +++++++ 17 files changed, 151 insertions(+), 41 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 4805d490f42..8483b5dee2f 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -41,6 +41,7 @@ export const de = { "startup.routing.native": "Natives OpenAI", "startup.routing.customLocal": "Benutzerdefiniertes lokales Gateway", "startup.routing.customRemote": "Benutzerdefiniertes Remote-Gateway", + "startup.routing.unknown": "Unbekanntes oder ungültiges Routing", "startup.restartProtection": "Neustartschutz", "startup.preference": "Start bei Bedarf", "startup.enabled": "Aktiviert", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 55c7700c945..4b151bd4a2a 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -50,6 +50,7 @@ export const en = { "startup.routing.native": "Native OpenAI", "startup.routing.customLocal": "Custom local gateway", "startup.routing.customRemote": "Custom remote gateway", + "startup.routing.unknown": "Unknown or invalid routing", "startup.restartProtection": "Restart protection", "startup.preference": "On-demand startup", "startup.enabled": "Enabled", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 538cec86561..c33f5445cb0 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -50,6 +50,7 @@ export const ja: Record = { "startup.routing.native": "OpenAI ネイティブ", "startup.routing.customLocal": "カスタムローカルゲートウェイ", "startup.routing.customRemote": "カスタム遠隔ゲートウェイ", + "startup.routing.unknown": "不明または無効なルーティング", "startup.restartProtection": "再起動保護", "startup.preference": "オンデマンド起動", "startup.enabled": "有効", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 42c8958436a..6f383219cec 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -45,6 +45,7 @@ export const ko: Record = { "startup.routing.native": "OpenAI 네이티브", "startup.routing.customLocal": "사용자 지정 로컬 게이트웨이", "startup.routing.customRemote": "사용자 지정 원격 게이트웨이", + "startup.routing.unknown": "알 수 없거나 잘못된 라우팅", "startup.restartProtection": "재부팅 보호", "startup.preference": "필요 시 자동 시작", "startup.enabled": "켜짐", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 45b9939f7c6..6653f56a4bb 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -50,6 +50,7 @@ export const ru: Record = { "startup.routing.native": "Нативный OpenAI", "startup.routing.customLocal": "Пользовательский локальный шлюз", "startup.routing.customRemote": "Пользовательский удалённый шлюз", + "startup.routing.unknown": "Неизвестная или недопустимая маршрутизация", "startup.restartProtection": "Защита перезапуска", "startup.preference": "Запуск по требованию", "startup.enabled": "Включён", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7010c5a8f86..b4b1859e712 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -45,6 +45,7 @@ export const zh: Record = { "startup.routing.native": "OpenAI 原生", "startup.routing.customLocal": "自定义本地网关", "startup.routing.customRemote": "自定义远程网关", + "startup.routing.unknown": "未知或无效的路由", "startup.restartProtection": "重启保护", "startup.preference": "按需启动", "startup.enabled": "已启用", diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index 151bd4ef7bb..e0bffab39e9 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -15,7 +15,7 @@ interface SettingsData { hostname: string; startupHealth?: { status: "native" | "protected" | "at-risk"; - routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote"; + routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown"; autostartEnabled: boolean; shimCoverage: "full" | "cli-only" | "none"; diagnosticStale: boolean; diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index 62da14ac966..61a1c995a19 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -8,7 +8,7 @@ type StartupProtection = "service" | "shim" | "none"; interface StartupHealthData { status: StartupStatus; - routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote"; + routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown"; routingInjected: boolean; localRoutingDependency: boolean; autostartEnabled: boolean; @@ -178,7 +178,8 @@ export default function Startup({ apiBase }: { apiBase: string }) { const routingKey: TKey = data?.routingKind === "opencodex-local" ? "startup.routing.proxy" : data?.routingKind === "custom-local" ? "startup.routing.customLocal" : data?.routingKind === "custom-remote" ? "startup.routing.customRemote" - : "startup.routing.native"; + : data?.routingKind === "unknown" ? "startup.routing.unknown" + : "startup.routing.native"; const statusClass = failed ? "startup-hero--risk" diff --git a/src/cli/index.ts b/src/cli/index.ts index 9455cf9de15..bf085b7fd7c 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -430,7 +430,8 @@ async function handleUninstall() { if (process.platform === "win32") { await runStep("Windows tray removed", async () => { const { getWindowsTrayStatus, uninstallWindowsTray } = await import("../tray/windows"); - if (!getWindowsTrayStatus().installed && !getWindowsTrayStatus().stale) return false; + const tray = getWindowsTrayStatus(); + if (!tray.installed && !tray.stale && !tray.running) return false; uninstallWindowsTray(); }); } diff --git a/src/codex/autostart-health.ts b/src/codex/autostart-health.ts index 3d0f26cfdd9..389c3d3d215 100644 --- a/src/codex/autostart-health.ts +++ b/src/codex/autostart-health.ts @@ -61,7 +61,9 @@ const COMMANDS = { export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth { const shimEffective = inputs.autostartEnabled && inputs.shimHealthy; const routingInjected = inputs.routingKind === "opencodex-local"; - const localRoutingDependency = inputs.routingKind === "opencodex-local" || inputs.routingKind === "custom-local"; + const localRoutingDependency = inputs.routingKind === "opencodex-local" + || inputs.routingKind === "custom-local" + || inputs.routingKind === "unknown"; // Script launchers never cover Codex Desktop/app-server surfaces. This is // intentionally conservative on every OS and for WSL-shared Codex homes. const shimCoverage: ShimCoverage = !shimEffective @@ -83,7 +85,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth : "at-risk"; const recommendedCommand = status !== "at-risk" ? null - : inputs.routingKind === "custom-local" + : inputs.routingKind === "custom-local" || inputs.routingKind === "unknown" ? COMMANDS.restoreNative : inputs.serviceSupported ? COMMANDS.installService @@ -127,6 +129,7 @@ export function startupHealthSummary(health: StartupHealth): string { ? "custom remote Codex routing (no local restart dependency)" : "native Codex routing (no opencodex restart dependency)"; if (health.protection === "service") return "protected by background service"; + if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${health.commands.restoreNative}' to restore native routing)`; if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${health.commands.restoreNative}' to restore native routing)`; if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 5e8c19328e0..8471842bf73 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -161,14 +161,15 @@ export function hasInjectedOpenaiBaseUrl(content: string): boolean { return false; } -export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote"; +export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown"; function rootTomlString(content: string, key: string): string | null { const lines = content.split("\n"); const firstTable = lines.findIndex(line => /^\s*\[/.test(line)); const rootLines = lines.slice(0, firstTable === -1 ? lines.length : firstTable); const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); + const keyToken = `(?:${escaped}|\"${escaped}\"|'${escaped}')`; + const pattern = new RegExp(`^\\s*${keyToken}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); for (const line of rootLines) { const match = pattern.exec(line); if (match?.[1]) return match[1].trim(); @@ -178,8 +179,10 @@ function rootTomlString(content: string, key: string): string | null { function providerTableString(content: string, provider: string, key: string): string | null { const lines = content.split("\n"); - const header = `[model_providers.${provider}]`; - const start = lines.findIndex(line => line.trim() === header); + const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const providerToken = `(?:${escapedProvider}|\"${escapedProvider}\"|'${escapedProvider}')`; + const header = new RegExp(`^\\s*\\[\\s*(?:model_providers|\"model_providers\"|'model_providers')\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`); + const start = lines.findIndex(line => header.test(line)); if (start === -1) return null; const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); @@ -190,12 +193,25 @@ function providerTableString(content: string, provider: string, key: string): st return null; } -function isLoopbackUrl(value: string): boolean { +type RoutingEndpointKind = "local" | "remote" | "unknown"; + +function classifyRoutingEndpoint(value: string): RoutingEndpointKind { try { - const hostname = new URL(value).hostname.toLowerCase(); - return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; + const url = new URL(value); + if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown"; + const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, ""); + if (!hostname) return "unknown"; + if (hostname === "localhost" || hostname.endsWith(".localhost")) return "local"; + if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0") return "local"; + const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname); + if (ipv4) { + const octets = ipv4.slice(1).map(Number); + if (octets.some(octet => octet > 255)) return "unknown"; + if (octets[0] === 127) return "local"; + } + return "remote"; } catch { - return false; + return "unknown"; } } @@ -203,17 +219,21 @@ function isLoopbackUrl(value: string): boolean { export function classifyCodexRouting(content: string): CodexRoutingKind { const rootBaseUrl = rootTomlString(content, "openai_base_url"); if (rootBaseUrl) { + const endpoint = classifyRoutingEndpoint(rootBaseUrl); + if (endpoint === "unknown") return "unknown"; if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local"; - if (!isLoopbackUrl(rootBaseUrl)) return "custom-remote"; - return "custom-local"; + return endpoint === "local" ? "custom-local" : "custom-remote"; } const rootProvider = rootTomlString(content, "model_provider"); if (rootProvider) { const providerBaseUrl = providerTableString(content, rootProvider, "base_url"); if (providerBaseUrl) { + const endpoint = classifyRoutingEndpoint(providerBaseUrl); + if (endpoint === "unknown") return "unknown"; if (rootProvider === "opencodex") return "opencodex-local"; - return isLoopbackUrl(providerBaseUrl) ? "custom-local" : "custom-remote"; + return endpoint === "local" ? "custom-local" : "custom-remote"; } + if (rootProvider === "opencodex") return "unknown"; } return "native"; } @@ -246,7 +266,7 @@ export function getCodexRoutingKind(): CodexRoutingKind { try { return classifyCodexRouting(readFileSync(path, "utf8")); } catch { - return "native"; + return "unknown"; } } diff --git a/src/server/startup-health-cache.ts b/src/server/startup-health-cache.ts index 94dde788462..e4c7fa83ebd 100644 --- a/src/server/startup-health-cache.ts +++ b/src/server/startup-health-cache.ts @@ -21,7 +21,9 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH rebootSafe: false, protection: "none", diagnosticStale: true, - recommendedCommand: value.routingKind === "custom-local" ? value.commands.restoreNative : value.commands.installService, + recommendedCommand: value.routingKind === "custom-local" || value.routingKind === "unknown" + ? value.commands.restoreNative + : value.commands.installService, }; } diff --git a/src/service.ts b/src/service.ts index dcb28dc22df..30d9f2a8987 100644 --- a/src/service.ts +++ b/src/service.ts @@ -865,6 +865,7 @@ export interface WindowsServiceDiagnosticInputs { schedulerEnabled: boolean; schedulerAssetsHealthy: boolean; nativeStatus: "started" | "stopped" | "nonexistent" | "unknown"; + nativeBackendRecorded: boolean; staleBakedPaths: boolean; nativeRepairAssetsOnly: boolean; diagnostics: string; @@ -875,6 +876,7 @@ export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticI const conflict = inputs.schedulerInstalled && nativeInstalled; const stale = inputs.staleBakedPaths || (inputs.schedulerInstalled && !inputs.schedulerAssetsHealthy) + || (nativeInstalled && !inputs.nativeBackendRecorded) || (inputs.nativeStatus === "nonexistent" && inputs.nativeRepairAssetsOnly); const backend = inputs.schedulerInstalled ? "scheduler" : nativeInstalled ? "native" : null; const enabled = inputs.schedulerInstalled ? inputs.schedulerEnabled : inputs.nativeStatus === "started"; @@ -935,11 +937,13 @@ export function diagnoseService(): ServiceDiagnostic { const schedulerAssets = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()].every(existsSync) && windowsTaskRegistrationHealthy(schedulerXml); const nativeStatus = statusWinswRaw(); + const nativeBackendRecorded = readServiceInstallState()?.backend === "native"; return deriveWindowsServiceDiagnostic({ schedulerInstalled, schedulerEnabled, schedulerAssetsHealthy: schedulerAssets, nativeStatus, + nativeBackendRecorded, staleBakedPaths: bakedServicePathsDiagnostic() !== null, nativeRepairAssetsOnly: Boolean(winswStatusSummary()), diagnostics, diff --git a/src/tray/windows.ts b/src/tray/windows.ts index b4791377ca5..8b9b1e4b408 100644 --- a/src/tray/windows.ts +++ b/src/tray/windows.ts @@ -206,11 +206,47 @@ function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): stri const output = runRegistry(["query", RUN_KEY, "/v", runValue, "/reg:64"]); return parseWindowsTrayRunValue(output, runValue); } catch (error) { - if ((error as { status?: unknown })?.status === 1) return null; + if ((error as { status?: unknown })?.status === 1) { + // reg.exe also uses exit 1 for access/query failures. Only treat the + // value as absent after proving that its parent key is readable. + try { + runRegistry(["query", RUN_KEY, "/reg:64"]); + return null; + } catch { /* fall through to the fail-closed error */ } + } throw new Error("Unable to verify the owned Windows tray registry value; refusing to change persistence."); } } +function runRegistryAsync(args: string[]): Promise { + return new Promise((resolvePromise, rejectPromise) => { + execFile(registryExe(), args, { + encoding: "utf8", + timeout: 2_000, + windowsHide: true, + maxBuffer: 64 * 1024, + }, (error, stdout) => { + if (error) rejectPromise(error); + else resolvePromise(stdout.trim()); + }); + }); +} + +async function readOwnedRunValueAsync(runValue = windowsTrayRunValue(getConfigDir())): Promise { + try { + const output = await runRegistryAsync(["query", RUN_KEY, "/v", runValue, "/reg:64"]); + return parseWindowsTrayRunValue(output, runValue); + } catch (error) { + if (Number((error as { code?: unknown }).code) === 1) { + try { + await runRegistryAsync(["query", RUN_KEY, "/reg:64"]); + return null; + } catch { /* fall through to the fail-closed error */ } + } + throw new Error("Unable to verify Windows tray registry status."); + } +} + function readHeartbeat(): { pid: number; hostPid?: number; timestamp: number } | null { try { const heartbeat = JSON.parse(readFileSync(trayHeartbeatPath(), "utf8").replace(/^\uFEFF/, "")) as { pid?: unknown; hostPid?: unknown; timestamp?: unknown }; @@ -257,14 +293,30 @@ function waitForTrayExit(previous: ReturnType, timeoutMs = return !heartbeatProcessAlive() && powershellExited && hostExited; } +export function windowsTrayRegistrationIsStale(inputs: { + registered: boolean; + registrationOwned: boolean; + running: boolean; + heartbeatFresh: boolean; +}): boolean { + if (!inputs.registered && inputs.running) return true; + if (inputs.registered && !inputs.registrationOwned) return true; + return inputs.running && !inputs.heartbeatFresh; +} + function trayStatusFrom(registered: string | null): WindowsTrayStatus { const state = readState(); const heartbeat = readHeartbeat(); const running = heartbeatProcessAlive(heartbeat); - const stale = registered !== null && (!state - || registered !== state.runCommand - || [state.bun, state.cli, state.script].some(path => !existsSync(path))) - || (running && (!heartbeat || Date.now() - heartbeat.timestamp > 15_000)); + const registrationOwned = state !== null + && registered === state.runCommand + && [state.bun, state.cli, state.script].every(path => existsSync(path)); + const stale = windowsTrayRegistrationIsStale({ + registered: registered !== null, + registrationOwned, + running, + heartbeatFresh: Boolean(heartbeat && Date.now() - heartbeat.timestamp <= 15_000), + }); const installed = registered !== null && state !== null && registered === state.runCommand && !stale; const summary = registered === null ? running ? "unregistered tray process is still running" : "not installed" @@ -288,18 +340,7 @@ export async function getWindowsTrayStatusAsync(): Promise { return { supported: false, installed: false, running: false, stale: false, summary: `unsupported on ${process.platform}` }; } const runValue = windowsTrayRunValue(getConfigDir()); - const registered = await new Promise((resolvePromise, rejectPromise) => { - execFile(registryExe(), ["query", RUN_KEY, "/v", runValue, "/reg:64"], { - encoding: "utf8", - timeout: 2_000, - windowsHide: true, - maxBuffer: 64 * 1024, - }, (error, stdout) => { - if (!error) return resolvePromise(parseWindowsTrayRunValue(stdout, runValue)); - if ((error as { code?: unknown }).code === 1) return resolvePromise(null); - rejectPromise(new Error("Unable to verify Windows tray registry status.")); - }); - }); + const registered = await readOwnedRunValueAsync(runValue); return trayStatusFrom(registered); } @@ -446,9 +487,8 @@ export function startWindowsTray(): WindowsTrayStatus { export function stopWindowsTray(): WindowsTrayStatus { assertWindows(); - const state = readState(); let previous = readHeartbeat(); - if (state) { + if (previous) { previous = signalTrayStop(); } if (!waitForTrayExit(previous)) throw new Error("The tray did not exit after the stop signal. Its login registration was preserved."); @@ -463,7 +503,7 @@ export function uninstallWindowsTray(): WindowsTrayStatus { throw new Error(`Refusing to remove a foreign or unowned HKCU Run value named ${state?.runValue ?? windowsTrayRunValue(getConfigDir())}.`); } let previous = readHeartbeat(); - if (state) { + if (previous) { previous = signalTrayStop(); } if (!waitForTrayExit(previous)) throw new Error("The tray did not exit; refusing to remove its owned registration or state."); diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index d03967a851c..9c95b1ef0a5 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -79,7 +79,16 @@ describe("Codex startup health", () => { ].join("\n"))).toBe(true); expect(hasInjectedCodexRouting('openai_base_url = "http://127.0.0.1:10100/v1"')).toBe(false); expect(classifyCodexRouting('openai_base_url = "http://127.0.0.1:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('"openai_base_url" = "http://127.0.0.2:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "http://0.0.0.0:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "http://[::]:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "not-a-url"')).toBe("unknown"); expect(classifyCodexRouting('openai_base_url = "https://gateway.example/v1"')).toBe("custom-remote"); + expect(classifyCodexRouting([ + 'model_provider = "gateway"', + '[model_providers."gateway"]', + 'base_url = "http://127.0.0.2:10100/v1"', + ].join("\n"))).toBe("custom-local"); expect(classifyCodexRouting([ "[features]", 'model_provider = "opencodex"', @@ -120,6 +129,12 @@ describe("Codex startup health", () => { status: "native", localRoutingDependency: false, }); + expect(deriveStartupHealth({ ...base, routingKind: "unknown", serviceInstalled: true, serviceViable: true })).toMatchObject({ + status: "at-risk", + rebootSafe: false, + protection: "none", + recommendedCommand: "ocx restore", + }); }); test("exposes a secret-free startup health DTO to the dashboard", async () => { diff --git a/tests/service.test.ts b/tests/service.test.ts index 34d29a030b5..2cd87416e42 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -496,6 +496,7 @@ describe("service diagnostics", () => { schedulerEnabled: false, schedulerAssetsHealthy: true, nativeStatus: "nonexistent" as const, + nativeBackendRecorded: false, staleBakedPaths: false, nativeRepairAssetsOnly: false, diagnostics: "logs: test", @@ -506,12 +507,12 @@ describe("service diagnostics", () => { expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true })).toMatchObject({ viable: false, enabled: false }); expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, staleBakedPaths: true })).toMatchObject({ viable: false, stale: true }); expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, nativeStatus: "started" })).toMatchObject({ viable: false, conflict: true }); - expect(deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped" })).toMatchObject({ installed: true, viable: false, running: false }); + expect(deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped" })).toMatchObject({ installed: true, viable: false, startable: false, stale: true, running: false }); expect(deriveWindowsServiceDiagnostic({ ...base, nativeRepairAssetsOnly: true })).toMatchObject({ installed: false, viable: false, stale: true }); }); test("a stopped healthy WinSW service remains startable from the tray", () => { - const stoppedNative = deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped" }); + const stoppedNative = deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped", nativeBackendRecorded: true }); expect(serviceStartableFromTray(stoppedNative)).toBe(true); expect(serviceStartableFromTray({ ...stoppedNative, stale: true })).toBe(false); expect(serviceStartableFromTray({ ...stoppedNative, conflict: true })).toBe(false); diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index be9d13cedf4..ee2d217c27d 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -7,6 +7,7 @@ import { windowsTrayProcessArgs, windowsTrayRunValue, windowsTrayStatePathsOwned, + windowsTrayRegistrationIsStale, type WindowsTrayEntry, } from "../src/tray/windows"; import { handleManagementAPI } from "../src/server/management-api"; @@ -64,6 +65,21 @@ describe("Windows tray packaging and command safety", () => { }, home)).toBe(false); }); + test("treats a live unregistered tray as stale so uninstall cannot skip it", () => { + expect(windowsTrayRegistrationIsStale({ + registered: false, + registrationOwned: false, + running: true, + heartbeatFresh: true, + })).toBe(true); + expect(windowsTrayRegistrationIsStale({ + registered: false, + registrationOwned: false, + running: false, + heartbeatFresh: false, + })).toBe(false); + }); + test("normalizes equivalent homes to one owned Run value", () => { expect(windowsTrayRunValue("C:\\Users\\Test\\.opencodex")) .toBe(windowsTrayRunValue("C:\\Users\\Test\\.opencodex\\.")); @@ -128,6 +144,7 @@ describe("Windows tray packaging and command safety", () => { expect(tray).toContain("previousScriptBytes"); expect(tray).toContain('windowsTrayProcessArgs(currentEntry(), "Stop")'); expect(tray).not.toContain("spawnTray(state)"); + expect(tray).toContain('runRegistry(["query", RUN_KEY, "/reg:64"])'); const updateSources = [ join(root, "src", "update", "index.ts"), From 6de12f72af7b16c5d3df73f00f947d46c873d39a Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:28:40 +0900 Subject: [PATCH 04/11] fix: keep lifecycle diagnostics backend-consistent --- src/codex/inject.ts | 26 +++++++++++++++++--------- src/service.ts | 14 ++++++++++---- tests/autostart-health.test.ts | 10 ++++++++-- tests/service.test.ts | 13 ++++++++++--- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 8471842bf73..dff7fc04c74 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -163,13 +163,17 @@ export function hasInjectedOpenaiBaseUrl(content: string): boolean { export type CodexRoutingKind = "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown"; +function tomlStringPattern(key: string): RegExp { + const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const keyToken = `(?:${escaped}|\"${escaped}\"|'${escaped}')`; + return new RegExp(`^\\s*${keyToken}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); +} + function rootTomlString(content: string, key: string): string | null { const lines = content.split("\n"); const firstTable = lines.findIndex(line => /^\s*\[/.test(line)); const rootLines = lines.slice(0, firstTable === -1 ? lines.length : firstTable); - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const keyToken = `(?:${escaped}|\"${escaped}\"|'${escaped}')`; - const pattern = new RegExp(`^\\s*${keyToken}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); + const pattern = tomlStringPattern(key); for (const line of rootLines) { const match = pattern.exec(line); if (match?.[1]) return match[1].trim(); @@ -177,15 +181,18 @@ function rootTomlString(content: string, key: string): string | null { return null; } -function providerTableString(content: string, provider: string, key: string): string | null { - const lines = content.split("\n"); +function providerTableStart(lines: string[], provider: string): number { const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const providerToken = `(?:${escapedProvider}|\"${escapedProvider}\"|'${escapedProvider}')`; const header = new RegExp(`^\\s*\\[\\s*(?:model_providers|\"model_providers\"|'model_providers')\\s*\\.\\s*${providerToken}\\s*\\]\\s*(?:#.*)?$`); - const start = lines.findIndex(line => header.test(line)); + return lines.findIndex(line => header.test(line)); +} + +function providerTableString(content: string, provider: string, key: string): string | null { + const lines = content.split("\n"); + const start = providerTableStart(lines, provider); if (start === -1) return null; - const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*[\"']([^\"']+)[\"']\\s*(?:#.*)?$`); + const pattern = tomlStringPattern(key); for (let index = start + 1; index < lines.length && !/^\s*\[/.test(lines[index]); index += 1) { const match = pattern.exec(lines[index]); if (match?.[1]) return match[1].trim(); @@ -226,6 +233,7 @@ export function classifyCodexRouting(content: string): CodexRoutingKind { } const rootProvider = rootTomlString(content, "model_provider"); if (rootProvider) { + const providerTableExists = providerTableStart(content.split("\n"), rootProvider) !== -1; const providerBaseUrl = providerTableString(content, rootProvider, "base_url"); if (providerBaseUrl) { const endpoint = classifyRoutingEndpoint(providerBaseUrl); @@ -233,7 +241,7 @@ export function classifyCodexRouting(content: string): CodexRoutingKind { if (rootProvider === "opencodex") return "opencodex-local"; return endpoint === "local" ? "custom-local" : "custom-remote"; } - if (rootProvider === "opencodex") return "unknown"; + if (rootProvider === "opencodex" || providerTableExists || rootProvider !== "openai") return "unknown"; } return "native"; } diff --git a/src/service.ts b/src/service.ts index 30d9f2a8987..072eb37887f 100644 --- a/src/service.ts +++ b/src/service.ts @@ -865,7 +865,7 @@ export interface WindowsServiceDiagnosticInputs { schedulerEnabled: boolean; schedulerAssetsHealthy: boolean; nativeStatus: "started" | "stopped" | "nonexistent" | "unknown"; - nativeBackendRecorded: boolean; + recordedBackend: ServiceBackend | null; staleBakedPaths: boolean; nativeRepairAssetsOnly: boolean; diagnostics: string; @@ -874,9 +874,12 @@ export interface WindowsServiceDiagnosticInputs { export function deriveWindowsServiceDiagnostic(inputs: WindowsServiceDiagnosticInputs): ServiceDiagnostic { const nativeInstalled = inputs.nativeStatus !== "nonexistent"; const conflict = inputs.schedulerInstalled && nativeInstalled; + const backendStateMismatch = inputs.schedulerInstalled + ? inputs.recordedBackend !== "scheduler" + : nativeInstalled && inputs.recordedBackend !== "native"; const stale = inputs.staleBakedPaths || (inputs.schedulerInstalled && !inputs.schedulerAssetsHealthy) - || (nativeInstalled && !inputs.nativeBackendRecorded) + || backendStateMismatch || (inputs.nativeStatus === "nonexistent" && inputs.nativeRepairAssetsOnly); const backend = inputs.schedulerInstalled ? "scheduler" : nativeInstalled ? "native" : null; const enabled = inputs.schedulerInstalled ? inputs.schedulerEnabled : inputs.nativeStatus === "started"; @@ -937,13 +940,16 @@ export function diagnoseService(): ServiceDiagnostic { const schedulerAssets = [windowsServiceScriptPath(), windowsLauncherVbsPath(), windowsTaskXmlPath()].every(existsSync) && windowsTaskRegistrationHealthy(schedulerXml); const nativeStatus = statusWinswRaw(); - const nativeBackendRecorded = readServiceInstallState()?.backend === "native"; + const installState = readServiceInstallState(); + const recordedBackend: ServiceBackend | null = !installState + ? null + : installState.backend === "native" ? "native" : "scheduler"; return deriveWindowsServiceDiagnostic({ schedulerInstalled, schedulerEnabled, schedulerAssetsHealthy: schedulerAssets, nativeStatus, - nativeBackendRecorded, + recordedBackend, staleBakedPaths: bakedServicePathsDiagnostic() !== null, nativeRepairAssetsOnly: Boolean(winswStatusSummary()), diagnostics, diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index 9c95b1ef0a5..86aae4e0772 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -85,10 +85,16 @@ describe("Codex startup health", () => { expect(classifyCodexRouting('openai_base_url = "not-a-url"')).toBe("unknown"); expect(classifyCodexRouting('openai_base_url = "https://gateway.example/v1"')).toBe("custom-remote"); expect(classifyCodexRouting([ - 'model_provider = "gateway"', + '"model_provider" = "gateway"', '[model_providers."gateway"]', - 'base_url = "http://127.0.0.2:10100/v1"', + '"base_url" = "http://127.0.0.2:10100/v1"', ].join("\n"))).toBe("custom-local"); + expect(classifyCodexRouting([ + 'model_provider = "gateway"', + '[model_providers.gateway]', + ].join("\n"))).toBe("unknown"); + expect(classifyCodexRouting('model_provider = "missing-custom"')).toBe("unknown"); + expect(classifyCodexRouting('model_provider = "openai"')).toBe("native"); expect(classifyCodexRouting([ "[features]", 'model_provider = "opencodex"', diff --git a/tests/service.test.ts b/tests/service.test.ts index 2cd87416e42..c4f85385d5f 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -496,14 +496,14 @@ describe("service diagnostics", () => { schedulerEnabled: false, schedulerAssetsHealthy: true, nativeStatus: "nonexistent" as const, - nativeBackendRecorded: false, + recordedBackend: null, staleBakedPaths: false, nativeRepairAssetsOnly: false, diagnostics: "logs: test", }; test("fails closed for disabled, stale, conflicting, stopped, and ghost Windows services", () => { - expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true })).toMatchObject({ viable: true, backend: "scheduler" }); + expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, recordedBackend: "scheduler" })).toMatchObject({ viable: true, backend: "scheduler" }); expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true })).toMatchObject({ viable: false, enabled: false }); expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, staleBakedPaths: true })).toMatchObject({ viable: false, stale: true }); expect(deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: true, nativeStatus: "started" })).toMatchObject({ viable: false, conflict: true }); @@ -512,13 +512,20 @@ describe("service diagnostics", () => { }); test("a stopped healthy WinSW service remains startable from the tray", () => { - const stoppedNative = deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped", nativeBackendRecorded: true }); + const stoppedNative = deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "stopped", recordedBackend: "native" }); expect(serviceStartableFromTray(stoppedNative)).toBe(true); expect(serviceStartableFromTray({ ...stoppedNative, stale: true })).toBe(false); expect(serviceStartableFromTray({ ...stoppedNative, conflict: true })).toBe(false); expect(serviceStartableFromTray(deriveWindowsServiceDiagnostic({ ...base, nativeStatus: "unknown" }))).toBe(false); const disabledScheduler = deriveWindowsServiceDiagnostic({ ...base, schedulerInstalled: true, schedulerEnabled: false }); expect(serviceStartableFromTray(disabledScheduler)).toBe(false); + const mismatchedScheduler = deriveWindowsServiceDiagnostic({ + ...base, + schedulerInstalled: true, + schedulerEnabled: true, + recordedBackend: "native", + }); + expect(mismatchedScheduler).toMatchObject({ backend: "scheduler", stale: true, viable: false, startable: false }); }); test("status summary exposes the service log path", () => { From afd473c08a8d625388f46bf77b67cb151cb0f39c Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:35:02 +0900 Subject: [PATCH 05/11] fix: reject ambiguous local routing state --- src/codex/inject.ts | 22 ++++++++++++++++++---- src/service.ts | 23 ++++++++++++++++++++--- tests/autostart-health.test.ts | 2 ++ tests/service.test.ts | 16 +++++++++++++++- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/codex/inject.ts b/src/codex/inject.ts index dff7fc04c74..a9c738cf71d 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -202,6 +202,19 @@ function providerTableString(content: string, provider: string, key: string): st type RoutingEndpointKind = "local" | "remote" | "unknown"; +function ipv4Octets(hostname: string): number[] | null { + const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname); + if (dotted) { + const octets = dotted.slice(1).map(Number); + return octets.some(octet => octet > 255) ? null : octets; + } + const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname); + if (!mapped) return null; + const high = Number.parseInt(mapped[1], 16); + const low = Number.parseInt(mapped[2], 16); + return [high >>> 8, high & 0xff, low >>> 8, low & 0xff]; +} + function classifyRoutingEndpoint(value: string): RoutingEndpointKind { try { const url = new URL(value); @@ -210,12 +223,13 @@ function classifyRoutingEndpoint(value: string): RoutingEndpointKind { if (!hostname) return "unknown"; if (hostname === "localhost" || hostname.endsWith(".localhost")) return "local"; if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0") return "local"; - const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname); - if (ipv4) { - const octets = ipv4.slice(1).map(Number); - if (octets.some(octet => octet > 255)) return "unknown"; + const octets = ipv4Octets(hostname); + if (octets) { + if (octets.every(octet => octet === 0)) return "local"; if (octets[0] === 127) return "local"; + return "remote"; } + if (/^::ffff:/i.test(hostname)) return "unknown"; return "remote"; } catch { return "unknown"; diff --git a/src/service.ts b/src/service.ts index 072eb37887f..26c7b9844c4 100644 --- a/src/service.ts +++ b/src/service.ts @@ -88,7 +88,7 @@ function normalizePathForCompare(path: string): string { return process.platform === "win32" ? resolved.toLowerCase() : resolved; } -interface ServiceInstallState { +export interface ServiceInstallState { version: 1 | 2; codexHome: string; opencodexHome: string; @@ -101,6 +101,23 @@ interface ServiceInstallState { winswSha256?: string; } +export function parseServiceInstallState(value: unknown): ServiceInstallState | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const state = value as Record; + if (state.version !== 1 && state.version !== 2) return null; + if (typeof state.codexHome !== "string" || state.codexHome.length === 0) return null; + if (typeof state.opencodexHome !== "string" || state.opencodexHome.length === 0) return null; + for (const key of ["bunPath", "cliPath", "winswVersion", "winswSha256"] as const) { + if (state[key] !== undefined && (typeof state[key] !== "string" || state[key].length === 0)) return null; + } + if (state.version === 1) { + if (state.backend !== undefined) return null; + } else if (state.backend !== "scheduler" && state.backend !== "native") { + return null; + } + return state as unknown as ServiceInstallState; +} + function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void { const { bun, cli } = cliEntry(); const state: ServiceInstallState = { @@ -124,8 +141,8 @@ function writeServiceInstallState(backend: ServiceBackend = "scheduler"): void { function readServiceInstallState(): ServiceInstallState | null { for (const path of serviceStatePaths()) { try { - const parsed = JSON.parse(readFileSync(path, "utf8")) as ServiceInstallState; - if (parsed.version === 1 || parsed.version === 2) return parsed; + const parsed = parseServiceInstallState(JSON.parse(readFileSync(path, "utf8"))); + if (parsed) return parsed; } catch { /* try the next known state path */ } diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index 86aae4e0772..7a71475e3ce 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -82,6 +82,8 @@ describe("Codex startup health", () => { expect(classifyCodexRouting('"openai_base_url" = "http://127.0.0.2:10100/v1"')).toBe("custom-local"); expect(classifyCodexRouting('openai_base_url = "http://0.0.0.0:10100/v1"')).toBe("custom-local"); expect(classifyCodexRouting('openai_base_url = "http://[::]:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "http://[::ffff:127.0.0.1]:10100/v1"')).toBe("custom-local"); + expect(classifyCodexRouting('openai_base_url = "http://[::ffff:127.1.2.3]:10100/v1"')).toBe("custom-local"); expect(classifyCodexRouting('openai_base_url = "not-a-url"')).toBe("unknown"); expect(classifyCodexRouting('openai_base_url = "https://gateway.example/v1"')).toBe("custom-remote"); expect(classifyCodexRouting([ diff --git a/tests/service.test.ts b/tests/service.test.ts index c4f85385d5f..afc9f4e1eba 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { saveConfig } from "../src/config"; import { windowsEnvIndirectBatchValue } from "../src/lib/win-paths"; -import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsServiceScript, buildWindowsTaskXml, deriveWindowsServiceDiagnostic, normalizeServiceSubcommand, resolveServiceListenPort, serviceLogPath, serviceStartableFromTray, serviceStatusSummary, windowsTaskRegistrationHealthy } from "../src/service"; +import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsServiceScript, buildWindowsTaskXml, deriveWindowsServiceDiagnostic, normalizeServiceSubcommand, parseServiceInstallState, resolveServiceListenPort, serviceLogPath, serviceStartableFromTray, serviceStatusSummary, windowsTaskRegistrationHealthy } from "../src/service"; import { serviceApiTokenFilePath } from "../src/lib/service-secrets"; import type { OcxConfig } from "../src/types"; @@ -528,6 +528,20 @@ describe("service diagnostics", () => { expect(mismatchedScheduler).toMatchObject({ backend: "scheduler", stale: true, viable: false, startable: false }); }); + test("rejects malformed service backend state instead of defaulting it to scheduler", () => { + const valid = { + version: 2, + codexHome: "C:\\codex", + opencodexHome: "C:\\opencodex", + backend: "scheduler", + }; + expect(parseServiceInstallState(valid)?.backend).toBe("scheduler"); + expect(parseServiceInstallState({ ...valid, backend: "garbage" })).toBeNull(); + expect(parseServiceInstallState({ ...valid, backend: undefined })).toBeNull(); + expect(parseServiceInstallState({ ...valid, version: 1, backend: "scheduler" })).toBeNull(); + expect(parseServiceInstallState({ ...valid, version: 1, backend: undefined })?.version).toBe(1); + }); + test("status summary exposes the service log path", () => { const summary = serviceStatusSummary(); From 2ad1130f95edf205979a5db1cc4a19286ee3c3b5 Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:38:37 +0900 Subject: [PATCH 06/11] fix(windows): distinguish missing tray registry keys --- src/tray/windows.ts | 39 ++++++++++++++++++++++++++++++++------ tests/windows-tray.test.ts | 11 +++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/tray/windows.ts b/src/tray/windows.ts index 8b9b1e4b408..1fd0f11fa2d 100644 --- a/src/tray/windows.ts +++ b/src/tray/windows.ts @@ -8,6 +8,7 @@ import { durableBunPath } from "../lib/bun-runtime"; import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl"; const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; +const RUN_PARENT_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion"; const TRAY_STATE_VERSION = 1; const FOREIGN_RUN_VALUE = ""; @@ -201,6 +202,27 @@ export function parseWindowsTrayRunValue(output: string, runValue: string): stri return match[2].trim(); } +export function windowsRegistryParentShowsRunKey(output: string): boolean { + const expected = RUN_KEY.toLowerCase(); + return output.split(/\r?\n/).some(line => line.trim().toLowerCase() + .replace(/^hkey_current_user\\/, "hkcu\\") === expected); +} + +function syncRegistryAbsenceIsProven(): boolean { + try { + runRegistry(["query", RUN_KEY, "/reg:64"]); + return true; + } catch (runError) { + if ((runError as { status?: unknown })?.status !== 1) return false; + try { + const parent = runRegistry(["query", RUN_PARENT_KEY, "/reg:64"]); + return !windowsRegistryParentShowsRunKey(parent); + } catch { + return false; + } + } +} + function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): string | null { try { const output = runRegistry(["query", RUN_KEY, "/v", runValue, "/reg:64"]); @@ -208,11 +230,9 @@ function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): stri } catch (error) { if ((error as { status?: unknown })?.status === 1) { // reg.exe also uses exit 1 for access/query failures. Only treat the - // value as absent after proving that its parent key is readable. - try { - runRegistry(["query", RUN_KEY, "/reg:64"]); - return null; - } catch { /* fall through to the fail-closed error */ } + // value as absent after proving Run is readable or does not exist under + // a readable CurrentVersion parent. + if (syncRegistryAbsenceIsProven()) return null; } throw new Error("Unable to verify the owned Windows tray registry value; refusing to change persistence."); } @@ -241,7 +261,14 @@ async function readOwnedRunValueAsync(runValue = windowsTrayRunValue(getConfigDi try { await runRegistryAsync(["query", RUN_KEY, "/reg:64"]); return null; - } catch { /* fall through to the fail-closed error */ } + } catch (runError) { + if (Number((runError as { code?: unknown }).code) === 1) { + try { + const parent = await runRegistryAsync(["query", RUN_PARENT_KEY, "/reg:64"]); + if (!windowsRegistryParentShowsRunKey(parent)) return null; + } catch { /* fall through to the fail-closed error */ } + } + } } throw new Error("Unable to verify Windows tray registry status."); } diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index ee2d217c27d..5b66190fdb8 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -8,6 +8,7 @@ import { windowsTrayRunValue, windowsTrayStatePathsOwned, windowsTrayRegistrationIsStale, + windowsRegistryParentShowsRunKey, type WindowsTrayEntry, } from "../src/tray/windows"; import { handleManagementAPI } from "../src/server/management-api"; @@ -93,6 +94,16 @@ describe("Windows tray packaging and command safety", () => { expect(parseWindowsTrayRunValue("unexpected output", value)).not.toBeNull(); }); + test("distinguishes a missing Run key from an unreadable existing key", () => { + const parent = [ + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion", + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer", + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", + ].join("\r\n"); + expect(windowsRegistryParentShowsRunKey(parent)).toBe(true); + expect(windowsRegistryParentShowsRunKey(parent.replace(/\\Run\r?\n?$/, ""))).toBe(false); + }); + test("PowerShell controller uses mutex/event shutdown and bans command evaluation", () => { const typescript = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows.ts"), "utf8"); const source = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows-tray.ps1"), "utf8"); From 5d0c50896674b5f52e5fb542dcfbf75e61779b83 Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 14:44:43 +0900 Subject: [PATCH 07/11] fix(windows): keep tray recovery controls actionable --- gui/src/pages/Startup.tsx | 10 ++++++---- src/cli/help.ts | 3 ++- src/cli/index.ts | 3 +-- src/tray/windows-tray.ps1 | 1 - tests/cli-help.test.ts | 7 +++++++ tests/windows-tray.test.ts | 1 + 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index 61a1c995a19..6b63079158e 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -255,9 +255,11 @@ export default function Startup({ apiBase }: { apiBase: string }) {
{t("startup.shim")}{t("startup.shimHint")}
@@ -292,7 +294,7 @@ export default function Startup({ apiBase }: { apiBase: string }) { {!trayLoading && !trayError && tray?.running && !tray.stale && ( )} - {!trayLoading && !trayError && tray?.installed && !tray.stale && ( + {!trayLoading && !trayError && tray && (tray.installed || tray.stale) && ( )} diff --git a/src/cli/help.ts b/src/cli/help.ts index 579a87fd9a9..edc578575d0 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -50,11 +50,12 @@ const helpEntries: Record = { details: ["Use `remove` as an alias for `uninstall`."], }, tray: { - usage: "ocx tray [--json]", + usage: "ocx tray [--json] [--no-start]", summary: "Install and control the Windows status tray icon.", details: [ "The tray starts at Windows login and provides one-click proxy controls.", "Tray start/stop controls the icon only; use its menu to start or stop the proxy.", + "--no-start (install only) installs the tray without launching it immediately.", ], }, ensure: { usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." }, diff --git a/src/cli/index.ts b/src/cli/index.ts index bf085b7fd7c..dcc6605bdec 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -26,7 +26,7 @@ import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSele import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness"; import { stopProxy } from "../lib/process-control"; import { loadServiceTokenFromFile } from "../lib/service-secrets"; -import { diagnoseService, serviceCommand, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service"; +import { diagnoseService, serviceCommand, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service"; import { startupHealthSummary } from "../codex/autostart-health"; import { drainAndShutdown, startServer } from "../server"; import { injectSystemEnv, revertSystemEnv } from "../server/system-env"; @@ -311,7 +311,6 @@ async function handleTrayProxyStart(): Promise { return; } const service = diagnoseService(); - const { serviceStartableFromTray } = await import("../service"); const serviceStartable = serviceStartableFromTray(service); if (service.installed && !serviceStartable) { console.error(`Cannot start from the tray because the installed service is not viable: ${service.summary}`); diff --git a/src/tray/windows-tray.ps1 b/src/tray/windows-tray.ps1 index a9994d35ff9..bc858418e5e 100644 --- a/src/tray/windows-tray.ps1 +++ b/src/tray/windows-tray.ps1 @@ -241,7 +241,6 @@ $logsItem.add_Click({ }) $exitItem.add_Click({ [System.Windows.Forms.Application]::Exit() }) $notify.add_DoubleClick({ Start-OcxCommand @("gui") }) -$menu.add_Opening({ Update-TrayState }) $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 3000 diff --git a/tests/cli-help.test.ts b/tests/cli-help.test.ts index dcdbfe22e45..4a4119a891a 100644 --- a/tests/cli-help.test.ts +++ b/tests/cli-help.test.ts @@ -45,6 +45,13 @@ describe("CLI subcommand help", () => { expect(result.stdout).toContain("Start the proxy server and sync models to Codex."); }); + test("tray help documents the install-only no-start flag", () => { + const result = runCli(["help", "tray"]); + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("--no-start"); + }); + test("unknown command with help flag remains an error", () => { const result = runCli(["foobar", "--help"]); expect(result.status).toBe(1); diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index 5b66190fdb8..10e73c02929 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -116,6 +116,7 @@ describe("Windows tray packaging and command safety", () => { expect(source).toContain("GetPathRoot"); expect(source).toContain("$heartbeat.hostPid = $HostPid"); expect(source).toContain('Start-OcxCommand @("__tray-restart")'); + expect(source).not.toContain("$menu.add_Opening({ Update-TrayState })"); expect(source).not.toContain("Invoke-Expression"); expect(source).not.toContain("taskkill"); expect(source).not.toContain("Stop-Process"); From 5515dac654a60db879e1ae563a7af656f6077ca7 Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 15:20:00 +0900 Subject: [PATCH 08/11] fix: address tray and startup review feedback --- bin/ocx.mjs | 29 ++++++---- gui/src/i18n/de.ts | 2 +- gui/src/i18n/ja.ts | 2 +- gui/src/i18n/ko.ts | 2 +- gui/src/i18n/ru.ts | 4 +- gui/src/pages/Dashboard.tsx | 3 +- gui/src/pages/Startup.tsx | 3 +- gui/src/startup-health-ui.ts | 12 ++++ src/cli/doctor.ts | 3 +- src/cli/index.ts | 89 +++++++++++++++-------------- src/cli/internal-dispatch.ts | 20 +++++++ src/cli/status.ts | 17 ++---- src/cli/tray-proxy.ts | 52 +++++++++++++++++ src/codex/autostart-health.ts | 36 +++++++----- src/tray/windows.ts | 69 +++++++++++++++------- src/update/index.ts | 26 ++++----- src/update/job.ts | 20 ++++--- src/update/tray-update-plan.d.mts | 18 ++++++ src/update/tray-update-plan.mjs | 38 ++++++++++++ tests/autostart-health.test.ts | 4 ++ tests/internal-cli-dispatch.test.ts | 36 ++++++++++++ tests/tray-proxy.test.ts | 88 ++++++++++++++++++++++++++++ tests/update-tray-handoff.test.ts | 62 ++++++++++++++++++++ tests/windows-tray.test.ts | 89 ++++++++++++++++++++++++++++- 24 files changed, 591 insertions(+), 133 deletions(-) create mode 100644 gui/src/startup-health-ui.ts create mode 100644 src/cli/internal-dispatch.ts create mode 100644 src/cli/tray-proxy.ts create mode 100644 src/update/tray-update-plan.d.mts create mode 100644 src/update/tray-update-plan.mjs create mode 100644 tests/internal-cli-dispatch.test.ts create mode 100644 tests/tray-proxy.test.ts create mode 100644 tests/update-tray-handoff.test.ts diff --git a/bin/ocx.mjs b/bin/ocx.mjs index edf794752d1..0218832defb 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -14,6 +14,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "../src/update/tray-update-plan.mjs"; const PKG = "@bitkyc08/opencodex"; const require = createRequire(import.meta.url); @@ -136,7 +137,9 @@ function runNpmSelfUpdate() { // unloads it permanently, so a successful update must reinstall it afterwards. const serviceStatePath = join(configDir(), "service-state.json"); const serviceWasInstalled = existsSync(serviceStatePath); - const trayBeforeUpdate = process.platform === "win32" ? trayInstallState() : { installed: false, running: false }; + const trayBeforeUpdate = planWindowsTrayUpdate( + process.platform === "win32" ? trayInstallState() : { installed: false, running: false }, + ); /** Read the backend from service-state.json so the update reinstalls the same one. */ function serviceReinstallArgs() { try { @@ -182,11 +185,17 @@ function runNpmSelfUpdate() { // and the runtime-port record too: a service-managed or orphaned proxy can be live // while ocx.pid is stale/missing. const launcher = fileURLToPath(import.meta.url); - if (trayBeforeUpdate.running) { + if (trayBeforeUpdate.stopBeforeReplacement) { console.log("⏹ Handing off the Windows tray before updating..."); - const stopped = runTrayLifecycle(launcher, "stop"); - if (stopped.status !== 0 || trayInstallState().running) { - runTrayLifecycle(launcher, "start"); + try { + handoffWindowsTrayForUpdate(trayBeforeUpdate, { + stop: () => { + const stopped = runTrayLifecycle(launcher, "stop"); + return { exitStatus: stopped.status, running: trayInstallState().running }; + }, + start: () => runTrayLifecycle(launcher, "start"), + }); + } catch { console.error("opencodex: could not stop the Windows tray; aborting before package replacement."); process.exit(1); } @@ -199,7 +208,7 @@ function runNpmSelfUpdate() { const stillHasRuntimeState = existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json")); if (stopRes.status !== 0 || stillHasRuntimeState) { - if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); + if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start"); console.error("opencodex: could not stop the running proxy; aborting the update. Run 'ocx stop' and retry."); process.exit(1); } @@ -222,14 +231,14 @@ function runNpmSelfUpdate() { if (res.status === 0) { console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`); repairCodexShimIfNeeded(); - if (trayBeforeUpdate.installed) { - const tray = spawnSync(process.execPath, [launcher, "tray", "install", ...(!trayBeforeUpdate.running ? ["--no-start"] : [])], { + if (trayBeforeUpdate.refreshAfterReplacement) { + const tray = spawnSync(process.execPath, [launcher, ...trayBeforeUpdate.installArgs], { stdio: "inherit", windowsHide: true, }); if (tray.status !== 0) { console.warn("opencodex: Windows tray refresh failed. Run: ocx tray install"); - if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); + if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start"); } } // The stop above unloaded any managed service; reinstall via the freshly-installed @@ -268,7 +277,7 @@ function runNpmSelfUpdate() { } process.exit(0); } - if (trayBeforeUpdate.running) runTrayLifecycle(launcher, "start"); + if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start"); console.error(`\nUpdate failed (${npm} exit ${res.status ?? "?"}). Try manually: ${npm} install -g ${PKG}@${tag}`); process.exit(1); } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 8483b5dee2f..8ef9176030a 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1,7 +1,7 @@ // German — generated from en.ts. Must match TKey set (compile-checked). export const de = { "nav.dashboard": "Übersicht", - "nav.startup": "Autostart", + "nav.startup": "Startsicherheit", "nav.providers": "Anbieter", "nav.models": "Modelle", "nav.combos": "Combos", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index c33f5445cb0..0bcc786c0c6 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -68,7 +68,7 @@ export const ja: Record = { "startup.shimHint": "対応する Codex スクリプトランチャーの起動時に ocx ensure を実行します。", "startup.healthy": "正常", "startup.cliOnly": "CLI のみ", - "startup.stale": "破損", + "startup.stale": "要更新", "startup.viable": "利用可能", "startup.unhealthy": "インストール済み・異常", "startup.conflict": "サービス競合", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 6f383219cec..d926c9984f5 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -63,7 +63,7 @@ export const ko: Record = { "startup.shimHint": "지원되는 Codex 스크립트 런처가 시작될 때 ocx ensure를 실행합니다.", "startup.healthy": "정상", "startup.cliOnly": "CLI 전용", - "startup.stale": "손상됨", + "startup.stale": "업데이트 필요", "startup.viable": "사용 가능", "startup.unhealthy": "설치됐지만 비정상", "startup.conflict": "서비스 충돌", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 6653f56a4bb..d89f3e9afa7 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -3,7 +3,7 @@ import type { TKey } from "./en"; export const ru: Record = { // sidebar / nav / common "nav.dashboard": "Дашборд", - "nav.startup": "Автозапуск", + "nav.startup": "Безопасность запуска", "nav.providers": "Провайдеры", "nav.models": "Модели", "nav.combos": "Комбо", @@ -68,7 +68,7 @@ export const ru: Record = { "startup.shimHint": "Запускает ocx ensure при запуске поддерживаемого скриптового лаунчера Codex.", "startup.healthy": "Исправен", "startup.cliOnly": "Только CLI", - "startup.stale": "Повреждён", + "startup.stale": "Устарел", "startup.viable": "Готов", "startup.unhealthy": "Установлен, но неисправен", "startup.conflict": "Конфликт служб", diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index e0bffab39e9..89c84698111 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -3,6 +3,7 @@ import { formatUptime } from "../formatUptime"; import { IconAlert, IconExternal, IconInfo, IconRefresh, IconX } from "../icons"; import { Trans } from "../i18n/provider"; import { useI18n, type TKey } from "../i18n/shared"; +import { startupRiskDetailKey } from "../startup-health-ui"; import { formatTokens } from "../format-tokens"; import { EmptyState, Select } from "../ui"; @@ -634,7 +635,7 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { {settings.startupHealth.diagnosticStale ? t("startup.staleData") - : t(settings.startupHealth.routingKind === "custom-local" ? "startup.riskDetailCustomLocal" : settings.startupHealth.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail")}{" "} + : t(startupRiskDetailKey(settings.startupHealth))}{" "} {t("startup.title")} diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index 6b63079158e..5231e6e8b36 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { IconAlert, IconCheck, IconPower, IconRefresh, IconTerminal } from "../icons"; import { useI18n, type TKey } from "../i18n/shared"; +import { startupRiskDetailKey } from "../startup-health-ui"; import { EmptyState } from "../ui"; type StartupStatus = "native" | "protected" | "at-risk"; @@ -219,7 +220,7 @@ export default function Startup({ apiBase }: { apiBase: string }) {

{failed ? t("startup.staleData") : data.status === "at-risk" - ? t(data.routingKind === "custom-local" ? "startup.riskDetailCustomLocal" : data.shimCoverage === "cli-only" ? "startup.riskDetailWindowsShim" : "startup.riskDetail") + ? t(startupRiskDetailKey(data)) : t("startup.safeDetail")}

diff --git a/gui/src/startup-health-ui.ts b/gui/src/startup-health-ui.ts new file mode 100644 index 00000000000..3d6546c94a1 --- /dev/null +++ b/gui/src/startup-health-ui.ts @@ -0,0 +1,12 @@ +import type { TKey } from "./i18n/shared"; + +export interface StartupRiskDetail { + routingKind: "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown"; + shimCoverage: "full" | "cli-only" | "none"; +} + +export function startupRiskDetailKey(health: StartupRiskDetail): TKey { + if (health.routingKind === "custom-local") return "startup.riskDetailCustomLocal"; + if (health.shimCoverage === "cli-only") return "startup.riskDetailWindowsShim"; + return "startup.riskDetail"; +} diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index d31939be51f..7c988ffcea2 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -410,7 +410,8 @@ export async function runDoctor(): Promise { const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive); const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present; if (!startup.rebootSafe) { - hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${startup.commands.installService}' or restore native routing with '${startup.commands.restoreNative}'.`); + const command = startup.recommendedCommand ?? startup.commands.restoreNative; + hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`); } if (anyDrvfs) { hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability."); diff --git a/src/cli/index.ts b/src/cli/index.ts index dcc6605bdec..ca1ddc47c9a 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,6 +20,8 @@ import { writeRuntimePort, } from "../config"; import { collectStatus } from "./status"; +import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch"; +import { runTrayProxyRestart, runTrayProxyStart } from "./tray-proxy"; import { installCrashGuards } from "../lib/crash-guard"; import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help"; import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports"; @@ -305,45 +307,43 @@ async function handleEnsure() { /** Fixed tray action: start the proxy without depending on codexAutoStart. */ async function handleTrayProxyStart(): Promise { - const live = await findLiveProxy(); - if (live) { - console.log(`Proxy already running on port ${live.port}.`); - return; - } - const service = diagnoseService(); - const serviceStartable = serviceStartableFromTray(service); - if (service.installed && !serviceStartable) { - console.error(`Cannot start from the tray because the installed service is not viable: ${service.summary}`); - console.error("Repair or remove the service before starting a direct proxy."); - process.exitCode = 1; - return; - } - if (serviceStartable) { - await serviceCommand("start"); - } else { - const config = loadConfig(); - const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100; - const child = spawn(process.execPath, startArgv(port), { - detached: true, - stdio: "ignore", - windowsHide: true, - env: { ...process.env, OCX_SERVICE: "1" }, - }); - child.unref(); - } - const started = await waitForProxy(); - if (!started) { - console.error("Proxy did not become healthy after the tray start action."); - process.exitCode = 1; - return; - } - console.log(`Proxy running on port ${started.port}.`); + const ok = await runTrayProxyStart({ + findLive: findLiveProxy, + diagnoseService: () => { + const service = diagnoseService(); + return { installed: service.installed, startable: serviceStartableFromTray(service), summary: service.summary }; + }, + startService: () => serviceCommand("start"), + startDirect: () => { + const config = loadConfig(); + const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100; + const child = spawn(process.execPath, startArgv(port), { + detached: true, + stdio: "ignore", + windowsHide: true, + env: { ...process.env, OCX_SERVICE: "1" }, + }); + child.unref(); + }, + waitForProxy, + info: message => console.log(message), + error: message => console.error(message), + }); + if (!ok) process.exitCode = 1; } async function handleTrayProxyRestart(): Promise { - await handleStop(); - if (process.exitCode && process.exitCode !== 0) return; - await handleTrayProxyStart(); + const ok = await runTrayProxyRestart({ + stop: async () => { + await handleStop(); + return !process.exitCode || process.exitCode === 0; + }, + start: async () => { + await handleTrayProxyStart(); + return !process.exitCode || process.exitCode === 0; + }, + }); + if (!ok) process.exitCode = 1; } async function handleStop() { @@ -696,21 +696,22 @@ switch (command) { break; } case "__tray-start": - await handleTrayProxyStart(); - break; case "__tray-restart": - await handleTrayProxyRestart(); + case "__startup-health": + await dispatchInternalCliCommand(command as InternalCliCommand, { + trayStart: handleTrayProxyStart, + trayRestart: handleTrayProxyRestart, + startupHealth: async () => { + const { collectStartupHealth } = await import("../codex/autostart-health"); + console.log(JSON.stringify(collectStartupHealth(loadConfig()))); + }, + }); break; case "__tray-host": { const { runWindowsTrayHost } = await import("../tray/windows"); await runWindowsTrayHost(); break; } - case "__startup-health": { - const { collectStartupHealth } = await import("../codex/autostart-health"); - console.log(JSON.stringify(collectStartupHealth(loadConfig()))); - break; - } case "__gui-update-worker": { const jobId = args[1]; if (!jobId) process.exit(1); diff --git a/src/cli/internal-dispatch.ts b/src/cli/internal-dispatch.ts new file mode 100644 index 00000000000..37f9a5484e9 --- /dev/null +++ b/src/cli/internal-dispatch.ts @@ -0,0 +1,20 @@ +export type InternalCliCommand = "__tray-start" | "__tray-restart" | "__startup-health"; + +export interface InternalCliHandlers { + trayStart: () => void | Promise; + trayRestart: () => void | Promise; + startupHealth: () => void | Promise; +} + +/** Dispatch fixed internal commands without accepting caller-selected process arguments. */ +export async function dispatchInternalCliCommand( + command: InternalCliCommand, + handlers: InternalCliHandlers, +): Promise { + switch (command) { + case "__tray-start": return void await handlers.trayStart(); + case "__tray-restart": return void await handlers.trayRestart(); + case "__startup-health": return void await handlers.startupHealth(); + default: throw new Error(`Unsupported internal CLI command: ${String(command)}`); + } +} diff --git a/src/cli/status.ts b/src/cli/status.ts index 5c32b897065..d434c0edd2a 100644 --- a/src/cli/status.ts +++ b/src/cli/status.ts @@ -4,7 +4,7 @@ import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../cod import { isOpencodexHealthz, probeHostname } from "../server/proxy-liveness"; import type { OcxConfig } from "../types"; import { diagnoseService } from "../service"; -import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health"; +import { collectStartupHealth, type StartupHealth } from "../codex/autostart-health"; import { getCodexRoutingKind } from "../codex/inject"; import { diagnoseCodexShim } from "../codex/shim"; @@ -123,19 +123,10 @@ export async function collectStatus(): Promise { const serviceSummary = service.summary; const codexShim = diagnoseCodexShim(); const codexShimSummary = codexShim.summary; - const startup = deriveStartupHealth({ + const startup = collectStartupHealth(config, { + service, + shim: codexShim, routingKind: getCodexRoutingKind(), - autostartEnabled: codexAutoStartEnabled(config), - serviceInstalled: service.installed, - serviceViable: service.viable, - serviceEnabled: service.enabled, - serviceRunning: service.running, - serviceStale: service.stale, - serviceConflict: service.conflict, - serviceSupported: service.supported, - shimInstalled: codexShim.installed, - shimHealthy: codexShim.healthy, - platform: process.platform, }); const codexPlugins = diagnoseCodexBundledPlugins(); const proxyLabel = pid && health.ok diff --git a/src/cli/tray-proxy.ts b/src/cli/tray-proxy.ts new file mode 100644 index 00000000000..00559fb451d --- /dev/null +++ b/src/cli/tray-proxy.ts @@ -0,0 +1,52 @@ +export interface TrayProxyLive { port: number } + +export interface TrayProxyServiceState { + installed: boolean; + startable: boolean; + summary: string; +} + +export interface TrayProxyStartIo { + findLive: () => Promise; + diagnoseService: () => TrayProxyServiceState; + startService: () => void | Promise; + startDirect: () => void | Promise; + waitForProxy: () => Promise; + info: (message: string) => void; + error: (message: string) => void; +} + +/** Side-effect coordinator for the tray's fixed proxy-start action. */ +export async function runTrayProxyStart(io: TrayProxyStartIo): Promise { + const live = await io.findLive(); + if (live) { + io.info(`Proxy already running on port ${live.port}.`); + return true; + } + + const service = io.diagnoseService(); + if (service.installed && !service.startable) { + io.error(`Cannot start from the tray because the installed service is not viable: ${service.summary}`); + io.error("Repair or remove the service before starting a direct proxy."); + return false; + } + + if (service.startable) await io.startService(); + else await io.startDirect(); + + const started = await io.waitForProxy(); + if (!started) { + io.error("Proxy did not become healthy after the tray start action."); + return false; + } + io.info(`Proxy running on port ${started.port}.`); + return true; +} + +export async function runTrayProxyRestart(io: { + stop: () => boolean | Promise; + start: () => boolean | Promise; +}): Promise { + if (!await io.stop()) return false; + return io.start(); +} diff --git a/src/codex/autostart-health.ts b/src/codex/autostart-health.ts index 389c3d3d215..5f25447aa7e 100644 --- a/src/codex/autostart-health.ts +++ b/src/codex/autostart-health.ts @@ -1,8 +1,8 @@ import { codexAutoStartEnabled } from "../config"; -import { diagnoseService } from "../service"; +import { diagnoseService, type ServiceDiagnostic } from "../service"; import type { OcxConfig } from "../types"; import { getCodexRoutingKind, type CodexRoutingKind } from "./inject"; -import { diagnoseCodexShim } from "./shim"; +import { diagnoseCodexShim, type CodexShimDiagnostic } from "./shim"; export type StartupProtection = "service" | "shim" | "none"; export type StartupHealthStatus = "native" | "protected" | "at-risk"; @@ -104,12 +104,21 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth }; } +export interface StartupHealthDiagnostics { + routingKind?: CodexRoutingKind; + service?: ServiceDiagnostic; + shim?: CodexShimDiagnostic; +} + /** Collect current machine state without mutating config, services, or shims. */ -export function collectStartupHealth(config: Pick): StartupHealth { - const shim = diagnoseCodexShim(); - const service = diagnoseService(); +export function collectStartupHealth( + config: Pick, + diagnostics: StartupHealthDiagnostics = {}, +): StartupHealth { + const shim = diagnostics.shim ?? diagnoseCodexShim(); + const service = diagnostics.service ?? diagnoseService(); return deriveStartupHealth({ - routingKind: getCodexRoutingKind(), + routingKind: diagnostics.routingKind ?? getCodexRoutingKind(), autostartEnabled: codexAutoStartEnabled(config), serviceInstalled: service.installed, serviceViable: service.viable, @@ -129,11 +138,12 @@ export function startupHealthSummary(health: StartupHealth): string { ? "custom remote Codex routing (no local restart dependency)" : "native Codex routing (no opencodex restart dependency)"; if (health.protection === "service") return "protected by background service"; - if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${health.commands.restoreNative}' to restore native routing)`; - if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${health.commands.restoreNative}' to restore native routing)`; - if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; - if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; - if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; - if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; - return `AT RISK after restart (no viable background service; run '${health.commands.installService}' or '${health.commands.restoreNative}')`; + const command = health.recommendedCommand ?? health.commands.restoreNative; + if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`; + if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`; + if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`; + if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`; + if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`; + if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${command}')`; + return `AT RISK after restart (no viable background service; run '${command}')`; } diff --git a/src/tray/windows.ts b/src/tray/windows.ts index 1fd0f11fa2d..de032ef75f2 100644 --- a/src/tray/windows.ts +++ b/src/tray/windows.ts @@ -208,14 +208,23 @@ export function windowsRegistryParentShowsRunKey(output: string): boolean { .replace(/^hkey_current_user\\/, "hkcu\\") === expected); } -function syncRegistryAbsenceIsProven(): boolean { +export type WindowsRegistryRunner = (args: string[]) => string; +export type WindowsRegistryAsyncRunner = (args: string[]) => Promise; + +function registryExitCode(error: unknown): number | null { + const value = error as { status?: unknown; code?: unknown }; + const code = Number(value.status ?? value.code); + return Number.isFinite(code) ? code : null; +} + +function syncRegistryAbsenceIsProven(run: WindowsRegistryRunner): boolean { try { - runRegistry(["query", RUN_KEY, "/reg:64"]); + run(["query", RUN_KEY, "/reg:64"]); return true; } catch (runError) { - if ((runError as { status?: unknown })?.status !== 1) return false; + if (registryExitCode(runError) !== 1) return false; try { - const parent = runRegistry(["query", RUN_PARENT_KEY, "/reg:64"]); + const parent = run(["query", RUN_PARENT_KEY, "/reg:64"]); return !windowsRegistryParentShowsRunKey(parent); } catch { return false; @@ -223,21 +232,25 @@ function syncRegistryAbsenceIsProven(): boolean { } } -function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): string | null { +export function readWindowsTrayRunValueWithRunner(runValue: string, run: WindowsRegistryRunner): string | null { try { - const output = runRegistry(["query", RUN_KEY, "/v", runValue, "/reg:64"]); + const output = run(["query", RUN_KEY, "/v", runValue, "/reg:64"]); return parseWindowsTrayRunValue(output, runValue); } catch (error) { - if ((error as { status?: unknown })?.status === 1) { + if (registryExitCode(error) === 1) { // reg.exe also uses exit 1 for access/query failures. Only treat the // value as absent after proving Run is readable or does not exist under // a readable CurrentVersion parent. - if (syncRegistryAbsenceIsProven()) return null; + if (syncRegistryAbsenceIsProven(run)) return null; } throw new Error("Unable to verify the owned Windows tray registry value; refusing to change persistence."); } } +function readOwnedRunValue(runValue = windowsTrayRunValue(getConfigDir())): string | null { + return readWindowsTrayRunValueWithRunner(runValue, runRegistry); +} + function runRegistryAsync(args: string[]): Promise { return new Promise((resolvePromise, rejectPromise) => { execFile(registryExe(), args, { @@ -252,28 +265,40 @@ function runRegistryAsync(args: string[]): Promise { }); } -async function readOwnedRunValueAsync(runValue = windowsTrayRunValue(getConfigDir())): Promise { +async function asyncRegistryAbsenceIsProven(run: WindowsRegistryAsyncRunner): Promise { try { - const output = await runRegistryAsync(["query", RUN_KEY, "/v", runValue, "/reg:64"]); + await run(["query", RUN_KEY, "/reg:64"]); + return true; + } catch (runError) { + if (registryExitCode(runError) !== 1) return false; + try { + const parent = await run(["query", RUN_PARENT_KEY, "/reg:64"]); + return !windowsRegistryParentShowsRunKey(parent); + } catch { + return false; + } + } +} + +export async function readWindowsTrayRunValueWithAsyncRunner( + runValue: string, + run: WindowsRegistryAsyncRunner, +): Promise { + try { + const output = await run(["query", RUN_KEY, "/v", runValue, "/reg:64"]); return parseWindowsTrayRunValue(output, runValue); } catch (error) { - if (Number((error as { code?: unknown }).code) === 1) { - try { - await runRegistryAsync(["query", RUN_KEY, "/reg:64"]); - return null; - } catch (runError) { - if (Number((runError as { code?: unknown }).code) === 1) { - try { - const parent = await runRegistryAsync(["query", RUN_PARENT_KEY, "/reg:64"]); - if (!windowsRegistryParentShowsRunKey(parent)) return null; - } catch { /* fall through to the fail-closed error */ } - } - } + if (registryExitCode(error) === 1 && await asyncRegistryAbsenceIsProven(run)) { + return null; } throw new Error("Unable to verify Windows tray registry status."); } } +async function readOwnedRunValueAsync(runValue = windowsTrayRunValue(getConfigDir())): Promise { + return readWindowsTrayRunValueWithAsyncRunner(runValue, runRegistryAsync); +} + function readHeartbeat(): { pid: number; hostPid?: number; timestamp: number } | null { try { const heartbeat = JSON.parse(readFileSync(trayHeartbeatPath(), "utf8").replace(/^\uFEFF/, "")) as { pid?: unknown; hostPid?: unknown; timestamp?: unknown }; diff --git a/src/update/index.ts b/src/update/index.ts index 935a43221f4..7b108529f16 100644 --- a/src/update/index.ts +++ b/src/update/index.ts @@ -3,6 +3,7 @@ import { readFileSync, readdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config"; +import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs"; /** * A `codex-history-backup-*.json` surviving a stop means the native-history restore was @@ -174,21 +175,18 @@ export async function runUpdate(): Promise { let trayWasRunning = false; if (process.platform === "win32") { try { - const { getWindowsTrayStatus, stopWindowsTray } = await import("../tray/windows"); + const { getWindowsTrayStatus, startWindowsTray, stopWindowsTray } = await import("../tray/windows"); const tray = getWindowsTrayStatus(); - trayWasInstalled = tray.installed; - trayWasRunning = tray.running; - if (tray.running) { - const stopped = stopWindowsTray(); - if (stopped.running) throw new Error("the tray still reports running after shutdown"); - } + const trayPlan = handoffWindowsTrayForUpdate(tray, { + stop: () => { + const stopped = stopWindowsTray(); + return { exitStatus: 0, running: stopped.running }; + }, + start: () => startWindowsTray(), + }); + trayWasInstalled = trayPlan.refreshAfterReplacement; + trayWasRunning = trayPlan.restoreOnFailure; } catch (error) { - if (trayWasRunning) { - try { - const { startWindowsTray } = await import("../tray/windows"); - startWindowsTray(); - } catch { /* preserve the handoff failure */ } - } console.error(`⚠️ Could not stop the Windows tray; aborting before package replacement: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); } @@ -268,7 +266,7 @@ export async function runUpdate(): Promise { console.warn(`⚠️ Shim repair skipped: ${e instanceof Error ? e.message : e}`); } if (trayWasInstalled) { - const trayArgs = [process.argv[1], "tray", "install", ...(!trayWasRunning ? ["--no-start"] : [])]; + const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs]; const tray = spawnSync(process.execPath, trayArgs, { stdio: "inherit", windowsHide: true }); if (tray.status === 0) { console.log("🔧 Refreshed Windows tray startup paths."); diff --git a/src/update/job.ts b/src/update/job.ts index 00352e394d5..f81c52a907b 100644 --- a/src/update/job.ts +++ b/src/update/job.ts @@ -20,6 +20,7 @@ import { updateCommandStr, } from "./index"; import { isNewer } from "./notify"; +import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs"; const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest"; const UPDATE_JOB_FILENAME = "update-job.json"; @@ -521,14 +522,17 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar if (process.platform === "win32") { try { - const { getWindowsTrayStatus, stopWindowsTray } = await import("../tray/windows"); + const { getWindowsTrayStatus, startWindowsTray, stopWindowsTray } = await import("../tray/windows"); const tray = getWindowsTrayStatus(); - trayWasInstalled = tray.installed; - trayWasRunning = tray.running; - if (tray.running) { - const stopped = stopWindowsTray(); - if (stopped.running) throw new Error("the tray still reports running after shutdown"); - } + const trayPlan = handoffWindowsTrayForUpdate(tray, { + stop: () => { + const stopped = stopWindowsTray(); + return { exitStatus: 0, running: stopped.running }; + }, + start: () => startWindowsTray(), + }); + trayWasInstalled = trayPlan.refreshAfterReplacement; + trayWasRunning = trayPlan.restoreOnFailure; } catch (error) { updateJob(job, { status: "failed", @@ -561,7 +565,7 @@ export async function runGuiUpdateWorker(jobId: string, channel: Channel, restar } if (trayWasInstalled) { - const trayArgs = [process.argv[1], "tray", "install", ...(!trayWasRunning ? ["--no-start"] : [])]; + const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs]; const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000); if (tray.status !== 0) { updateJob(job, {}, "Windows tray refresh failed; run 'ocx tray install'."); diff --git a/src/update/tray-update-plan.d.mts b/src/update/tray-update-plan.d.mts new file mode 100644 index 00000000000..9e9d45d2560 --- /dev/null +++ b/src/update/tray-update-plan.d.mts @@ -0,0 +1,18 @@ +export interface WindowsTrayUpdatePlan { + installed: boolean; + running: boolean; + stopBeforeReplacement: boolean; + restoreOnFailure: boolean; + refreshAfterReplacement: boolean; + installArgs: string[]; +} + +export declare function planWindowsTrayUpdate(status: { installed?: boolean; running?: boolean }): WindowsTrayUpdatePlan; +export declare function windowsTrayStopConfirmed(exitStatus: number | null, stillRunning: boolean): boolean; +export declare function handoffWindowsTrayForUpdate( + status: { installed?: boolean; running?: boolean }, + io: { + stop: () => { exitStatus: number | null; running: boolean }; + start: () => unknown; + }, +): WindowsTrayUpdatePlan; diff --git a/src/update/tray-update-plan.mjs b/src/update/tray-update-plan.mjs new file mode 100644 index 00000000000..695a3467aa7 --- /dev/null +++ b/src/update/tray-update-plan.mjs @@ -0,0 +1,38 @@ +/** + * Shared, side-effect-free contract for preserving the Windows tray across all + * updater entry points (npm launcher, CLI updater, and GUI worker). + */ +export function planWindowsTrayUpdate(status) { + const installed = status?.installed === true; + const running = status?.running === true; + return { + installed, + running, + stopBeforeReplacement: running, + restoreOnFailure: running, + refreshAfterReplacement: installed, + installArgs: ["tray", "install", ...(!running ? ["--no-start"] : [])], + }; +} + +export function windowsTrayStopConfirmed(exitStatus, stillRunning) { + return exitStatus === 0 && !stillRunning; +} + +/** Stop before replacement and restore best-effort when the handoff cannot be confirmed. */ +export function handoffWindowsTrayForUpdate(status, io) { + const plan = planWindowsTrayUpdate(status); + if (!plan.stopBeforeReplacement) return plan; + try { + const stopped = io.stop(); + if (!windowsTrayStopConfirmed(stopped.exitStatus, stopped.running)) { + throw new Error("the tray still reports running after shutdown"); + } + return plan; + } catch (error) { + if (plan.restoreOnFailure) { + try { io.start(); } catch { /* preserve the handoff failure */ } + } + throw error; + } +} diff --git a/tests/autostart-health.test.ts b/tests/autostart-health.test.ts index 7a71475e3ce..f26ed10e99f 100644 --- a/tests/autostart-health.test.ts +++ b/tests/autostart-health.test.ts @@ -30,6 +30,7 @@ describe("Codex startup health", () => { recommendedCommand: "ocx service install", }); expect(startupHealthSummary(health)).toContain("AT RISK"); + expect(startupHealthSummary(health)).toContain("ocx service install"); }); test("treats a background service as restart protection", () => { @@ -143,6 +144,9 @@ describe("Codex startup health", () => { protection: "none", recommendedCommand: "ocx restore", }); + const custom = deriveStartupHealth({ ...base, routingKind: "custom-local" }); + expect(startupHealthSummary(custom)).toContain("run 'ocx restore'"); + expect(startupHealthSummary(custom)).not.toContain("ocx service install"); }); test("exposes a secret-free startup health DTO to the dashboard", async () => { diff --git a/tests/internal-cli-dispatch.test.ts b/tests/internal-cli-dispatch.test.ts new file mode 100644 index 00000000000..5c64299ff87 --- /dev/null +++ b/tests/internal-cli-dispatch.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "bun:test"; +import { dispatchInternalCliCommand, type InternalCliCommand } from "../src/cli/internal-dispatch"; + +describe("internal CLI dispatcher", () => { + test.each([ + ["__tray-start", "trayStart"], + ["__tray-restart", "trayRestart"], + ["__startup-health", "startupHealth"], + ] as const)("routes %s to only %s", async (command, expected) => { + const calls: string[] = []; + await dispatchInternalCliCommand(command as InternalCliCommand, { + trayStart: async () => { calls.push("trayStart"); }, + trayRestart: async () => { calls.push("trayRestart"); }, + startupHealth: async () => { calls.push("startupHealth"); }, + }); + expect(calls).toEqual([expected]); + }); + + test("propagates an action failure to the CLI boundary", async () => { + await expect(dispatchInternalCliCommand("__tray-start", { + trayStart: async () => { throw new Error("start failed"); }, + trayRestart: () => {}, + startupHealth: () => {}, + })).rejects.toThrow("start failed"); + }); + + test("rejects an unknown runtime value without invoking a handler", async () => { + const calls: string[] = []; + await expect(dispatchInternalCliCommand("__invalid" as InternalCliCommand, { + trayStart: () => { calls.push("trayStart"); }, + trayRestart: () => { calls.push("trayRestart"); }, + startupHealth: () => { calls.push("startupHealth"); }, + })).rejects.toThrow("Unsupported internal CLI command"); + expect(calls).toEqual([]); + }); +}); diff --git a/tests/tray-proxy.test.ts b/tests/tray-proxy.test.ts new file mode 100644 index 00000000000..1ecf0130d77 --- /dev/null +++ b/tests/tray-proxy.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "bun:test"; +import { runTrayProxyRestart, runTrayProxyStart, type TrayProxyStartIo } from "../src/cli/tray-proxy"; + +function startIo(overrides: Partial = {}) { + const calls: string[] = []; + const io: TrayProxyStartIo = { + findLive: async () => null, + diagnoseService: () => ({ installed: false, startable: false, summary: "not installed" }), + startService: async () => { calls.push("service"); }, + startDirect: () => { calls.push("direct"); }, + waitForProxy: async () => ({ port: 10100 }), + info: message => { calls.push(`info:${message}`); }, + error: message => { calls.push(`error:${message}`); }, + ...overrides, + }; + return { io, calls }; +} + +describe("tray proxy coordinator", () => { + test("returns immediately when a proxy is already live", async () => { + const { io, calls } = startIo({ findLive: async () => ({ port: 20200 }) }); + expect(await runTrayProxyStart(io)).toBe(true); + expect(calls).toEqual(["info:Proxy already running on port 20200."]); + }); + + test("refuses an installed but unviable service instead of bypassing it", async () => { + const { io, calls } = startIo({ + diagnoseService: () => ({ installed: true, startable: false, summary: "stale" }), + }); + expect(await runTrayProxyStart(io)).toBe(false); + expect(calls.some(call => call.startsWith("error:Cannot start"))).toBe(true); + expect(calls).not.toContain("direct"); + expect(calls).not.toContain("service"); + }); + + test("uses a viable service and otherwise falls back to a direct start", async () => { + const service = startIo({ + diagnoseService: () => ({ installed: true, startable: true, summary: "healthy" }), + }); + expect(await runTrayProxyStart(service.io)).toBe(true); + expect(service.calls).toContain("service"); + expect(service.calls).not.toContain("direct"); + + const direct = startIo(); + expect(await runTrayProxyStart(direct.io)).toBe(true); + expect(direct.calls).toContain("direct"); + expect(direct.calls).not.toContain("service"); + }); + + test("fails when the selected start path never becomes healthy", async () => { + const { io, calls } = startIo({ waitForProxy: async () => null }); + expect(await runTrayProxyStart(io)).toBe(false); + expect(calls).toContain("direct"); + expect(calls.some(call => call.includes("did not become healthy"))).toBe(true); + }); + + test("propagates the selected start failure without trying an alternate path", async () => { + const service = startIo({ + diagnoseService: () => ({ installed: true, startable: true, summary: "healthy" }), + startService: async () => { service.calls.push("service"); throw new Error("service failed"); }, + }); + await expect(runTrayProxyStart(service.io)).rejects.toThrow("service failed"); + expect(service.calls).toContain("service"); + expect(service.calls).not.toContain("direct"); + + const direct = startIo({ + startDirect: () => { direct.calls.push("direct"); throw new Error("spawn failed"); }, + }); + await expect(runTrayProxyStart(direct.io)).rejects.toThrow("spawn failed"); + expect(direct.calls).toContain("direct"); + expect(direct.calls).not.toContain("service"); + }); + + test("restart never starts after a failed stop", async () => { + const calls: string[] = []; + expect(await runTrayProxyRestart({ + stop: async () => { calls.push("stop"); return false; }, + start: async () => { calls.push("start"); return true; }, + })).toBe(false); + expect(calls).toEqual(["stop"]); + + expect(await runTrayProxyRestart({ + stop: async () => true, + start: async () => { calls.push("start-after-stop"); return true; }, + })).toBe(true); + expect(calls).toContain("start-after-stop"); + }); +}); diff --git a/tests/update-tray-handoff.test.ts b/tests/update-tray-handoff.test.ts new file mode 100644 index 00000000000..7945ccd95a0 --- /dev/null +++ b/tests/update-tray-handoff.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate, windowsTrayStopConfirmed } from "../src/update/tray-update-plan.mjs"; + +describe("Windows tray update handoff contract", () => { + test("preserves an installed, running tray across replacement", () => { + expect(planWindowsTrayUpdate({ installed: true, running: true })).toEqual({ + installed: true, + running: true, + stopBeforeReplacement: true, + restoreOnFailure: true, + refreshAfterReplacement: true, + installArgs: ["tray", "install"], + }); + }); + + test("refreshes an installed, idle tray without starting it", () => { + expect(planWindowsTrayUpdate({ installed: true, running: false })).toMatchObject({ + stopBeforeReplacement: false, + restoreOnFailure: false, + refreshAfterReplacement: true, + installArgs: ["tray", "install", "--no-start"], + }); + }); + + test("does nothing for an absent tray and fails closed on incomplete stop", () => { + expect(planWindowsTrayUpdate({ installed: false, running: false })).toMatchObject({ + stopBeforeReplacement: false, + restoreOnFailure: false, + refreshAfterReplacement: false, + }); + expect(windowsTrayStopConfirmed(0, false)).toBe(true); + expect(windowsTrayStopConfirmed(1, false)).toBe(false); + expect(windowsTrayStopConfirmed(0, true)).toBe(false); + }); + + test("restores a previously running tray when stop throws or remains live", () => { + const calls: string[] = []; + expect(() => handoffWindowsTrayForUpdate({ installed: true, running: true }, { + stop: () => { calls.push("stop-throw"); throw new Error("registry status failed"); }, + start: () => { calls.push("restore-after-throw"); }, + })).toThrow("registry status failed"); + expect(calls).toEqual(["stop-throw", "restore-after-throw"]); + + expect(() => handoffWindowsTrayForUpdate({ installed: true, running: true }, { + stop: () => ({ exitStatus: 0, running: true }), + start: () => { calls.push("restore-still-live"); }, + })).toThrow("still reports running"); + expect(calls).toContain("restore-still-live"); + }); + + test("all three updater lanes consume the same tested plan", () => { + const root = join(import.meta.dir, ".."); + for (const path of ["src/update/index.ts", "src/update/job.ts", "bin/ocx.mjs"]) { + const source = readFileSync(join(root, path), "utf8"); + expect(source).toContain("planWindowsTrayUpdate"); + expect(source).toContain("handoffWindowsTrayForUpdate"); + expect(source).toContain("installArgs"); + } + }); +}); diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index 10e73c02929..e3cdd268322 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -4,6 +4,8 @@ import { join } from "node:path"; import { buildWindowsTrayRunCommand, parseWindowsTrayRunValue, + readWindowsTrayRunValueWithAsyncRunner, + readWindowsTrayRunValueWithRunner, windowsTrayProcessArgs, windowsTrayRunValue, windowsTrayStatePathsOwned, @@ -104,6 +106,90 @@ describe("Windows tray packaging and command safety", () => { expect(windowsRegistryParentShowsRunKey(parent.replace(/\\Run\r?\n?$/, ""))).toBe(false); }); + test("fails closed when registry absence cannot be proven", async () => { + const value = "OpenCodexTray-test"; + const statusError = (status: number) => Object.assign(new Error(`reg exit ${status}`), { status }); + const codeError = (code: number) => Object.assign(new Error(`reg exit ${code}`), { code }); + + expect(readWindowsTrayRunValueWithRunner(value, args => { + if (args.includes("/v")) throw statusError(1); + if (args[1]?.endsWith("\\Run")) return "readable"; + throw new Error("unexpected query"); + })).toBeNull(); + expect(() => readWindowsTrayRunValueWithRunner(value, () => { throw statusError(5); })) + .toThrow("refusing to change persistence"); + expect(() => readWindowsTrayRunValueWithRunner(value, args => { + if (args.includes("/v")) throw statusError(1); + throw statusError(5); + })).toThrow("refusing to change persistence"); + + await expect(readWindowsTrayRunValueWithAsyncRunner(value, async args => { + if (args.includes("/v")) throw codeError(1); + if (args[1]?.endsWith("\\Run")) return "readable"; + throw new Error("unexpected query"); + })).resolves.toBeNull(); + await expect(readWindowsTrayRunValueWithAsyncRunner(value, async () => { throw codeError(5); })) + .rejects.toThrow("Unable to verify Windows tray registry status"); + await expect(readWindowsTrayRunValueWithAsyncRunner(value, async args => { + if (args.includes("/v")) throw codeError(1); + throw codeError(5); + })).rejects.toThrow("Unable to verify Windows tray registry status"); + }); + + test("proves a missing Run key only through the readable parent path", async () => { + const value = "OpenCodexTray-test"; + const syncCalls: string[][] = []; + const syncResult = readWindowsTrayRunValueWithRunner(value, args => { + syncCalls.push(args); + if (args.includes("/v") || args[1]?.endsWith("\\Run")) { + throw Object.assign(new Error("missing"), { status: 1 }); + } + return "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer"; + }); + expect(syncResult).toBeNull(); + expect(syncCalls).toEqual([ + ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", value, "/reg:64"], + ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/reg:64"], + ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion", "/reg:64"], + ]); + + const asyncCalls: string[][] = []; + const asyncResult = await readWindowsTrayRunValueWithAsyncRunner(value, async args => { + asyncCalls.push(args); + if (args.includes("/v") || args[1]?.endsWith("\\Run")) { + throw Object.assign(new Error("missing"), { code: 1 }); + } + return "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer"; + }); + expect(asyncResult).toBeNull(); + expect(asyncCalls).toEqual(syncCalls); + + expect(() => readWindowsTrayRunValueWithRunner(value, args => { + if (args.includes("/v") || args[1]?.endsWith("\\Run")) { + throw Object.assign(new Error("unreadable"), { status: 1 }); + } + return "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + })).toThrow("refusing to change persistence"); + + await expect(readWindowsTrayRunValueWithAsyncRunner(value, async args => { + if (args.includes("/v") || args[1]?.endsWith("\\Run")) { + throw Object.assign(new Error("unreadable"), { code: 1 }); + } + return "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + })).rejects.toThrow("Unable to verify Windows tray registry status"); + + const parentFailureSync = (args: string[]): string => { + if (args.includes("/v") || args[1]?.endsWith("\\Run")) throw Object.assign(new Error("missing"), { status: 1 }); + throw Object.assign(new Error("parent unreadable"), { status: 5 }); + }; + expect(() => readWindowsTrayRunValueWithRunner(value, parentFailureSync)) + .toThrow("refusing to change persistence"); + await expect(readWindowsTrayRunValueWithAsyncRunner(value, async args => { + if (args.includes("/v") || args[1]?.endsWith("\\Run")) throw Object.assign(new Error("missing"), { code: 1 }); + throw Object.assign(new Error("parent unreadable"), { code: 5 }); + })).rejects.toThrow("Unable to verify Windows tray registry status"); + }); + test("PowerShell controller uses mutex/event shutdown and bans command evaluation", () => { const typescript = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows.ts"), "utf8"); const source = readFileSync(join(import.meta.dir, "..", "src", "tray", "windows-tray.ps1"), "utf8"); @@ -156,7 +242,8 @@ describe("Windows tray packaging and command safety", () => { expect(tray).toContain("previousScriptBytes"); expect(tray).toContain('windowsTrayProcessArgs(currentEntry(), "Stop")'); expect(tray).not.toContain("spawnTray(state)"); - expect(tray).toContain('runRegistry(["query", RUN_KEY, "/reg:64"])'); + expect(tray).toContain("return readWindowsTrayRunValueWithRunner(runValue, runRegistry)"); + expect(tray).toContain("return readWindowsTrayRunValueWithAsyncRunner(runValue, runRegistryAsync)"); const updateSources = [ join(root, "src", "update", "index.ts"), From 24f59966fbf2d4e4c5a3c9d8688b461436ab442c Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 15:30:02 +0900 Subject: [PATCH 09/11] fix(gui): ignore stale settings polls --- gui/src/pages/Dashboard.tsx | 25 ++++++++++++++++++++++--- gui/src/startup-health-ui.ts | 14 ++++++++++++++ tests/startup-health-ui.test.ts | 21 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/startup-health-ui.test.ts diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index 89c84698111..3f4af66ee71 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -3,7 +3,7 @@ import { formatUptime } from "../formatUptime"; import { IconAlert, IconExternal, IconInfo, IconRefresh, IconX } from "../icons"; import { Trans } from "../i18n/provider"; import { useI18n, type TKey } from "../i18n/shared"; -import { startupRiskDetailKey } from "../startup-health-ui"; +import { settingsPollMayCommit, startupRiskDetailKey } from "../startup-health-ui"; import { formatTokens } from "../format-tokens"; import { EmptyState, Select } from "../ui"; @@ -222,6 +222,9 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { const updateRetryRef = useRef(0); const updateRetryTimerRef = useRef(null); const updateRequestEpochRef = useRef(0); + const settingsRequestEpochRef = useRef(0); + const settingsMutationEpochRef = useRef(0); + const settingsMutationInFlightRef = useRef(false); const [updateCheck, setUpdateCheck] = useState(null); const [updateError, setUpdateError] = useState(null); const [updateJob, setUpdateJob] = useState(null); @@ -244,6 +247,8 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { useEffect(() => { const fetchData = async () => { + const settingsRequestEpoch = ++settingsRequestEpochRef.current; + const settingsMutationEpoch = settingsMutationEpochRef.current; try { const [hRes, pRes, sRes, scRes, shRes, uRes] = await Promise.all([ fetch(`${apiBase}/healthz`), @@ -255,7 +260,15 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { ]); setHealth(await hRes.json()); setProviders(await pRes.json()); - setSettings(await sRes.json()); + const nextSettings = await sRes.json() as SettingsData; + if (settingsPollMayCommit( + { request: settingsRequestEpoch, mutation: settingsMutationEpoch }, + { + request: settingsRequestEpochRef.current, + mutation: settingsMutationEpochRef.current, + mutationInFlight: settingsMutationInFlightRef.current, + }, + )) setSettings(nextSettings); setSidecar(await scRes.json()); // Old servers fall through to the SPA HTML for this route; don't let a parse // failure here take down the whole dashboard. @@ -296,7 +309,10 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { }; fetchData(); const interval = setInterval(fetchData, 5000); - return () => clearInterval(interval); + return () => { + clearInterval(interval); + settingsRequestEpochRef.current += 1; + }; }, [apiBase]); useEffect(() => { @@ -450,6 +466,7 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { if (!settings || settingsSaving) return; const next = !settings.codexAutoStart; setSettingsSaving(true); + settingsMutationInFlightRef.current = true; setSettings({ ...settings, codexAutoStart: next }); try { const res = await fetch(`${apiBase}/api/settings`, { @@ -459,11 +476,13 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { }); if (!res.ok) throw new Error("save failed"); const data = await res.json() as { codexAutoStart: boolean; startupHealth?: SettingsData["startupHealth"] }; + settingsMutationEpochRef.current += 1; setSettings(prev => prev ? { ...prev, codexAutoStart: data.codexAutoStart, startupHealth: data.startupHealth ?? prev.startupHealth } : prev); } catch { setSettings(prev => prev ? { ...prev, codexAutoStart: !next } : prev); setError(true); } finally { + settingsMutationInFlightRef.current = false; setSettingsSaving(false); } }; diff --git a/gui/src/startup-health-ui.ts b/gui/src/startup-health-ui.ts index 3d6546c94a1..7cd13478ce0 100644 --- a/gui/src/startup-health-ui.ts +++ b/gui/src/startup-health-ui.ts @@ -10,3 +10,17 @@ export function startupRiskDetailKey(health: StartupRiskDetail): TKey { if (health.shimCoverage === "cli-only") return "startup.riskDetailWindowsShim"; return "startup.riskDetail"; } + +export interface SettingsPollEpoch { + request: number; + mutation: number; +} + +export function settingsPollMayCommit( + started: SettingsPollEpoch, + current: SettingsPollEpoch & { mutationInFlight: boolean }, +): boolean { + return !current.mutationInFlight + && started.request === current.request + && started.mutation === current.mutation; +} diff --git a/tests/startup-health-ui.test.ts b/tests/startup-health-ui.test.ts new file mode 100644 index 00000000000..7d6d9398787 --- /dev/null +++ b/tests/startup-health-ui.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; +import { settingsPollMayCommit, startupRiskDetailKey } from "../gui/src/startup-health-ui"; + +describe("startup health UI decisions", () => { + test("selects the shared risk-detail message", () => { + expect(startupRiskDetailKey({ routingKind: "custom-local", shimCoverage: "none" })) + .toBe("startup.riskDetailCustomLocal"); + expect(startupRiskDetailKey({ routingKind: "opencodex-local", shimCoverage: "cli-only" })) + .toBe("startup.riskDetailWindowsShim"); + expect(startupRiskDetailKey({ routingKind: "unknown", shimCoverage: "none" })) + .toBe("startup.riskDetail"); + }); + + test("rejects stale or mutation-racing settings polls", () => { + const started = { request: 4, mutation: 2 }; + expect(settingsPollMayCommit(started, { request: 4, mutation: 2, mutationInFlight: false })).toBe(true); + expect(settingsPollMayCommit(started, { request: 5, mutation: 2, mutationInFlight: false })).toBe(false); + expect(settingsPollMayCommit(started, { request: 4, mutation: 3, mutationInFlight: false })).toBe(false); + expect(settingsPollMayCommit(started, { request: 4, mutation: 2, mutationInFlight: true })).toBe(false); + }); +}); From 5f4d837ae6688a207bba4b112ce9c6f67d4ddcd2 Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 16:19:36 +0900 Subject: [PATCH 10/11] feat(windows): add branded tray icons --- src/tray/assets/opencodex-tray-offline.ico | Bin 0 -> 82051 bytes src/tray/assets/opencodex-tray-online.ico | Bin 0 -> 95137 bytes src/tray/assets/opencodex-tray-warning.ico | Bin 0 -> 94883 bytes src/tray/assets/opencodex-tray.png | Bin 0 -> 173274 bytes src/tray/windows-tray.ps1 | 24 +++++++++++++-- src/tray/windows.ts | 33 +++++++++++++++++++-- tests/windows-tray.test.ts | 20 ++++++++++++- 7 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/tray/assets/opencodex-tray-offline.ico create mode 100644 src/tray/assets/opencodex-tray-online.ico create mode 100644 src/tray/assets/opencodex-tray-warning.ico create mode 100644 src/tray/assets/opencodex-tray.png diff --git a/src/tray/assets/opencodex-tray-offline.ico b/src/tray/assets/opencodex-tray-offline.ico new file mode 100644 index 0000000000000000000000000000000000000000..80848ab6fc294157312edca75b51aab275cd27b9 GIT binary patch literal 82051 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�opp+I7wX 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%0D# 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 z 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%0D(H+p6&*f0JFs{-KuW8{l(?N@L2GX}-1~hVW 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 literal 0 HcmV?d00001 diff --git a/src/tray/windows-tray.ps1 b/src/tray/windows-tray.ps1 index bc858418e5e..c7dfa58f7af 100644 --- a/src/tray/windows-tray.ps1 +++ b/src/tray/windows-tray.ps1 @@ -22,6 +22,22 @@ function Normalize-HomePath([string]$Value) { $OpenCodexHome = Normalize-HomePath $OpenCodexHome $CodexHome = Normalize-HomePath $CodexHome +$script:ownedIcons = New-Object System.Collections.Generic.List[System.Drawing.Icon] +function Load-TrayIcon([string]$Name, [System.Drawing.Icon]$Fallback) { + $path = Join-Path $OpenCodexHome $Name + if (-not [System.IO.File]::Exists($path)) { return $Fallback } + try { + $icon = New-Object System.Drawing.Icon($path) + [void]$script:ownedIcons.Add($icon) + return $icon + } catch { + return $Fallback + } +} +$onlineIcon = Load-TrayIcon "opencodex-tray-online.ico" ([System.Drawing.SystemIcons]::Information) +$warningIcon = Load-TrayIcon "opencodex-tray-warning.ico" ([System.Drawing.SystemIcons]::Warning) +$offlineIcon = Load-TrayIcon "opencodex-tray-offline.ico" ([System.Drawing.SystemIcons]::Error) + function Get-StableHash([string]$Value) { $sha = [System.Security.Cryptography.SHA256]::Create() try { @@ -187,16 +203,16 @@ function Update-TrayState { $startup = Read-JsonUrl "$origin/api/startup-health" $label = if ($startup.status -eq "at-risk") { "At risk" } elseif ($startup.status -eq "protected") { "Protected" } else { "Native routing" } $safetyItem.Text = "Restart safety: $label" - $notify.Icon = if ($startup.status -eq "at-risk") { [System.Drawing.SystemIcons]::Warning } else { [System.Drawing.SystemIcons]::Information } + $notify.Icon = if ($startup.status -eq "at-risk") { $warningIcon } else { $onlineIcon } } catch { $safetyItem.Text = "Restart safety: unavailable" - $notify.Icon = [System.Drawing.SystemIcons]::Information + $notify.Icon = $warningIcon } } else { $statusItem.Text = "Proxy: Offline" $safetyItem.Text = "Restart safety: start the proxy to inspect" $notify.Text = "opencodex: Offline" - $notify.Icon = [System.Drawing.SystemIcons]::Error + $notify.Icon = $offlineIcon $startItem.Enabled = $true $stopItem.Enabled = $false $restartItem.Enabled = $false @@ -252,6 +268,7 @@ $timer.add_Tick({ Update-TrayState }) $notify.ContextMenuStrip = $menu +$notify.Icon = $offlineIcon $notify.Visible = $true $notify.Text = "opencodex: Checking..." @@ -264,6 +281,7 @@ try { $timer.Dispose() $notify.Visible = $false $notify.Dispose() + foreach ($icon in $script:ownedIcons) { $icon.Dispose() } $menu.Dispose() try { Remove-Item -LiteralPath $heartbeatPath -Force -ErrorAction SilentlyContinue } catch { } try { $mutex.ReleaseMutex() } catch { } diff --git a/src/tray/windows.ts b/src/tray/windows.ts index de032ef75f2..914c0cd0a2e 100644 --- a/src/tray/windows.ts +++ b/src/tray/windows.ts @@ -11,6 +11,11 @@ const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; const RUN_PARENT_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion"; const TRAY_STATE_VERSION = 1; const FOREIGN_RUN_VALUE = ""; +const TRAY_ICON_FILES = [ + "opencodex-tray-online.ico", + "opencodex-tray-warning.ico", + "opencodex-tray-offline.ico", +] as const; export interface WindowsTrayEntry { bun: string; @@ -47,6 +52,10 @@ function installedTrayScriptPath(): string { return join(getConfigDir(), "opencodex-tray.ps1"); } +function installedTrayIconPaths(): string[] { + return TRAY_ICON_FILES.map(name => join(getConfigDir(), name)); +} + export function windowsTrayStatePathsOwned( state: Pick & { launcherPath?: string }, configDir = getConfigDir(), @@ -61,6 +70,10 @@ function sourceTrayScriptPath(): string { return join(import.meta.dir, "windows-tray.ps1"); } +function sourceTrayIconPaths(): string[] { + return TRAY_ICON_FILES.map(name => join(import.meta.dir, "assets", name)); +} + function currentCodexHome(): string { const raw = process.env.CODEX_HOME?.trim(); return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex"); @@ -362,7 +375,7 @@ function trayStatusFrom(registered: string | null): WindowsTrayStatus { const running = heartbeatProcessAlive(heartbeat); const registrationOwned = state !== null && registered === state.runCommand - && [state.bun, state.cli, state.script].every(path => existsSync(path)); + && [state.bun, state.cli, state.script, ...installedTrayIconPaths()].every(path => existsSync(path)); const stale = windowsTrayRegistrationIsStale({ registered: registered !== null, registrationOwned, @@ -459,7 +472,8 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus { assertWindows(); const entry = currentEntry(); const sourceScript = sourceTrayScriptPath(); - for (const path of [entry.bun, entry.cli, sourceScript]) { + const iconPairs = sourceTrayIconPaths().map((source, index) => ({ source, installed: installedTrayIconPaths()[index] })); + for (const path of [entry.bun, entry.cli, sourceScript, ...iconPairs.map(pair => pair.source)]) { if (!existsSync(path)) throw new Error(`Cannot install the tray because a required file is missing: ${path}`); } if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 }); @@ -473,6 +487,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus { if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) { throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`); } + if (!state && iconPairs.some(pair => existsSync(pair.installed))) { + throw new Error("Refusing to overwrite unowned Windows tray icon assets."); + } const wasRunning = heartbeatProcessAlive(); if (wasRunning && !state) { throw new Error("Refusing to replace an unowned running tray process. Exit it before installing."); @@ -484,11 +501,21 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus { const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null; const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null; + const previousIconBytes = new Map(iconPairs.map(pair => [ + pair.installed, + existsSync(pair.installed) ? readFileSync(pair.installed) : null, + ])); const restorePreviousInstall = () => { try { if (previousScriptBytes) replaceOwnedFile(entry.script, previousScriptBytes); else if (existsSync(entry.script)) unlinkSync(entry.script); } catch { /* rollback best-effort */ } + for (const [path, contents] of previousIconBytes) { + try { + if (contents) replaceOwnedFile(path, contents); + else if (existsSync(path)) unlinkSync(path); + } catch { /* rollback best-effort */ } + } try { if (previousStateBytes) replaceOwnedFile(trayStatePath(), previousStateBytes); else if (existsSync(trayStatePath())) unlinkSync(trayStatePath()); @@ -509,6 +536,7 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus { const hardenedDir = hardenSecretDir(getConfigDir(), { required: true }); if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence."); replaceOwnedFile(entry.script, readFileSync(sourceScript)); + for (const pair of iconPairs) replaceOwnedFile(pair.installed, readFileSync(pair.source)); runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]); writeState(entry, runValue, runCommand); } catch (error) { @@ -562,6 +590,7 @@ export function uninstallWindowsTray(): WindowsTrayStatus { if (existing) runRegistry(["delete", RUN_KEY, "/v", state?.runValue ?? windowsTrayRunValue(getConfigDir()), "/f", "/reg:64"]); const ownedPaths = [trayStatePath(), trayHeartbeatPath(), ...(state?.launcherPath ? [state.launcherPath] : [])]; if (state?.script && resolve(state.script) === resolve(installedTrayScriptPath())) ownedPaths.push(state.script); + if (state) ownedPaths.push(...installedTrayIconPaths()); for (const path of ownedPaths) { try { if (existsSync(path)) unlinkSync(path); } catch { /* best-effort */ } } diff --git a/tests/windows-tray.test.ts b/tests/windows-tray.test.ts index e3cdd268322..29e24576135 100644 --- a/tests/windows-tray.test.ts +++ b/tests/windows-tray.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { buildWindowsTrayRunCommand, @@ -202,12 +202,28 @@ describe("Windows tray packaging and command safety", () => { expect(source).toContain("GetPathRoot"); expect(source).toContain("$heartbeat.hostPid = $HostPid"); expect(source).toContain('Start-OcxCommand @("__tray-restart")'); + expect(source).toContain('Load-TrayIcon "opencodex-tray-online.ico"'); + expect(source).toContain('Load-TrayIcon "opencodex-tray-warning.ico"'); + expect(source).toContain('Load-TrayIcon "opencodex-tray-offline.ico"'); + expect(source).toContain("$notify.Icon = $offlineIcon"); expect(source).not.toContain("$menu.add_Opening({ Update-TrayState })"); expect(source).not.toContain("Invoke-Expression"); expect(source).not.toContain("taskkill"); expect(source).not.toContain("Stop-Process"); }); + test("ships branded multi-size Windows tray icons", () => { + const assets = join(import.meta.dir, "..", "src", "tray", "assets"); + for (const name of ["online", "warning", "offline"]) { + const path = join(assets, `opencodex-tray-${name}.ico`); + expect(existsSync(path)).toBe(true); + const bytes = readFileSync(path); + expect(bytes.readUInt16LE(0)).toBe(0); + expect(bytes.readUInt16LE(2)).toBe(1); + expect(bytes.readUInt16LE(4)).toBeGreaterThanOrEqual(7); + } + }); + test("serves tray status without blocking the proxy event loop", async () => { if (process.platform !== "win32") return; const url = new URL("http://localhost/api/windows-tray"); @@ -233,6 +249,8 @@ describe("Windows tray packaging and command safety", () => { const root = join(import.meta.dir, ".."); const tray = readFileSync(join(root, "src", "tray", "windows.ts"), "utf8"); expect(tray).toContain('join(getConfigDir(), "opencodex-tray.ps1")'); + expect(tray).toContain('join(import.meta.dir, "assets", name)'); + expect(tray).toContain("installedTrayIconPaths()"); expect(tray).toContain("const hardened = hardenSecretPath(temporary, { required: true })"); expect(tray).toContain("if (!hardened.ok)"); expect(tray).toContain("if (!hardenedDir.ok)"); From 1f6a6cdad758f2259303324c689bec3ff34add4a Mon Sep 17 00:00:00 2001 From: appcaster Date: Thu, 23 Jul 2026 16:32:45 +0900 Subject: [PATCH 11/11] feat(gui): add one-click startup installers --- .../src/content/docs/guides/web-dashboard.md | 1 + gui/src/i18n/de.ts | 7 +- gui/src/i18n/en.ts | 7 +- gui/src/i18n/ja.ts | 7 +- gui/src/i18n/ko.ts | 7 +- gui/src/i18n/ru.ts | 7 +- gui/src/i18n/zh.ts | 7 +- gui/src/pages/Startup.tsx | 68 +++++++++++++++---- gui/src/styles.css | 3 + src/server/management/config-routes.ts | 17 +++++ src/server/management/context.ts | 2 + src/server/startup-action-control.ts | 41 +++++++++++ structure/05_gui-and-management-api.md | 2 +- tests/startup-action-control.test.ts | 48 +++++++++++++ 14 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 src/server/startup-action-control.ts create mode 100644 tests/startup-action-control.test.ts diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index f26f9bc122d..5ab85e46f6a 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -89,6 +89,7 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints | --- | --- | | `GET` / `PUT /api/settings` | Read settings or toggle Codex autostart. | | `GET /api/startup-health` | Read secret-free routing, service, shim, and restart-safety diagnostics. | +| `POST /api/startup-action` | Install the background service or Codex launcher shim through fixed, allowlisted actions. | | `GET` / `POST /api/windows-tray` | Read or change the Windows tray installation and visible-process state. POST accepts `install`, `start`, `stop`, or `uninstall`. | | `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | | `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 8ef9176030a..b7b5d5fe77f 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -64,6 +64,11 @@ export const de = { "startup.unhealthy": "Installiert, aber fehlerhaft", "startup.conflict": "Dienstkonflikt", "startup.installedDisabled": "Installiert, aber deaktiviert", + "startup.install": "Installieren", + "startup.installing": "Wird installiert…", + "startup.serviceInstalled": "Hintergrunddienst wurde erfolgreich installiert.", + "startup.shimInstalled": "Codex-Launcher-Shim wurde erfolgreich installiert.", + "startup.installFailed": "Installation fehlgeschlagen:", "startup.tray.title": "Windows-Infobereich", "startup.tray.hint": "Installiert ein Anmeldesymbol für Proxy-Start, Stopp, Neustart, Dashboard und Status per Klick.", "startup.tray.login": "Infobereich bei Windows-Anmeldung starten", @@ -80,7 +85,7 @@ export const de = { "startup.tray.uninstall": "Anmeldesymbol entfernen", "startup.tray.error": "Die Windows-Infobereichsaktion ist fehlgeschlagen. Details: ocx tray status.", "startup.recovery": "Reparaturoptionen", - "startup.recoveryHint": "Führe einen Befehl im Terminal aus. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.", + "startup.recoveryHint": "Nutze die Ein-Klick-Installation oben oder kopiere einen Befehl für die manuelle Reparatur. Für Codex Desktop und Windows-Programme wird der Hintergrunddienst empfohlen.", "startup.command.service": "Empfohlen: dauerhafter Hintergrunddienst", "startup.command.shim": "Alternative: CLI-Launcher-Shim", "startup.command.native": "Ausfallsicher: natives Codex-Routing wiederherstellen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 4b151bd4a2a..b5ba13e26a7 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -73,6 +73,11 @@ export const en = { "startup.unhealthy": "Installed but unhealthy", "startup.conflict": "Service conflict", "startup.installedDisabled": "Installed but disabled", + "startup.install": "Install", + "startup.installing": "Installing…", + "startup.serviceInstalled": "Background service installed successfully.", + "startup.shimInstalled": "Codex launcher shim installed successfully.", + "startup.installFailed": "Installation failed:", "startup.tray.title": "Windows system tray", "startup.tray.hint": "Install a login tray icon for one-click proxy start, stop, restart, dashboard, and status controls.", "startup.tray.login": "Start tray at Windows login", @@ -89,7 +94,7 @@ export const en = { "startup.tray.uninstall": "Remove login tray", "startup.tray.error": "The Windows tray action failed. Check ocx tray status for details.", "startup.recovery": "Repair options", - "startup.recoveryHint": "Run one command in a terminal. The background service is recommended for Codex Desktop and Windows executables.", + "startup.recoveryHint": "Use the one-click installers above, or copy a command for manual repair. The background service is recommended for Codex Desktop and Windows executables.", "startup.command.service": "Recommended: persistent background service", "startup.command.shim": "Alternative: CLI launcher shim", "startup.command.native": "Fail-safe: restore native Codex routing", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 0bcc786c0c6..c2505fec665 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -73,6 +73,11 @@ export const ja: Record = { "startup.unhealthy": "インストール済み・異常", "startup.conflict": "サービス競合", "startup.installedDisabled": "インストール済み・無効", + "startup.install": "インストール", + "startup.installing": "インストール中…", + "startup.serviceInstalled": "バックグラウンドサービスをインストールしました。", + "startup.shimInstalled": "Codex ランチャー shim をインストールしました。", + "startup.installFailed": "インストールに失敗しました:", "startup.tray.title": "Windows システムトレイ", "startup.tray.hint": "ログイン時にトレイを起動し、プロキシの開始・停止・再起動・ダッシュボード・状態をクリックで操作します。", "startup.tray.login": "Windows ログイン時にトレイを開始", @@ -89,7 +94,7 @@ export const ja: Record = { "startup.tray.uninstall": "ログイントレイを削除", "startup.tray.error": "Windows トレイ操作に失敗しました。ocx tray status で詳細を確認してください。", "startup.recovery": "修復方法", - "startup.recoveryHint": "ターミナルでコマンドを1つ実行します。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。", + "startup.recoveryHint": "上のワンクリックインストールを使うか、手動修復用のコマンドをコピーできます。Codex Desktop と Windows 実行ファイルにはバックグラウンドサービスを推奨します。", "startup.command.service": "推奨: 永続バックグラウンドサービス", "startup.command.shim": "代替: CLI launcher shim", "startup.command.native": "安全策: Codex ネイティブルーティングを復元", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index d926c9984f5..2bb309e39ef 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -68,6 +68,11 @@ export const ko: Record = { "startup.unhealthy": "설치됐지만 비정상", "startup.conflict": "서비스 충돌", "startup.installedDisabled": "설치됐지만 꺼짐", + "startup.install": "설치하기", + "startup.installing": "설치 중…", + "startup.serviceInstalled": "백그라운드 서비스를 설치했습니다.", + "startup.shimInstalled": "Codex launcher shim을 설치했습니다.", + "startup.installFailed": "설치하지 못했습니다:", "startup.tray.title": "Windows 시스템 트레이", "startup.tray.hint": "로그인할 때 트레이 아이콘을 띄우고 프록시 시작·중지·재시작·대시보드·상태를 클릭으로 제어합니다.", "startup.tray.login": "Windows 로그인 시 트레이 시작", @@ -84,7 +89,7 @@ export const ko: Record = { "startup.tray.uninstall": "로그인 트레이 제거", "startup.tray.error": "Windows 트레이 작업에 실패했습니다. ocx tray status에서 상세 내용을 확인하세요.", "startup.recovery": "복구 방법", - "startup.recoveryHint": "터미널에서 명령 하나를 실행하세요. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.", + "startup.recoveryHint": "위의 원클릭 설치를 사용하거나 수동 복구 명령을 복사할 수 있습니다. Codex Desktop과 Windows 실행 파일에는 백그라운드 서비스를 권장합니다.", "startup.command.service": "권장: 영구 백그라운드 서비스", "startup.command.shim": "대안: CLI launcher shim", "startup.command.native": "안전 전환: Codex 네이티브 라우팅 복구", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index d89f3e9afa7..8d3a09af922 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -73,6 +73,11 @@ export const ru: Record = { "startup.unhealthy": "Установлен, но неисправен", "startup.conflict": "Конфликт служб", "startup.installedDisabled": "Установлен, но отключён", + "startup.install": "Установить", + "startup.installing": "Установка…", + "startup.serviceInstalled": "Фоновая служба успешно установлена.", + "startup.shimInstalled": "Launcher shim Codex успешно установлен.", + "startup.installFailed": "Не удалось установить:", "startup.tray.title": "Системный трей Windows", "startup.tray.hint": "Запускает значок при входе для управления запуском, остановкой, перезапуском, панелью и состоянием прокси.", "startup.tray.login": "Запускать трей при входе в Windows", @@ -89,7 +94,7 @@ export const ru: Record = { "startup.tray.uninstall": "Удалить трей входа", "startup.tray.error": "Действие Windows tray завершилось ошибкой. Подробности: ocx tray status.", "startup.recovery": "Варианты исправления", - "startup.recoveryHint": "Выполните одну команду в терминале. Для Codex Desktop и Windows рекомендуется фоновая служба.", + "startup.recoveryHint": "Используйте установку в один клик выше или скопируйте команду для ручного восстановления. Для Codex Desktop и Windows рекомендуется фоновая служба.", "startup.command.service": "Рекомендуется: постоянная фоновая служба", "startup.command.shim": "Альтернатива: CLI launcher shim", "startup.command.native": "Безопасный режим: восстановить нативную маршрутизацию Codex", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index b4b1859e712..dac142b5260 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -68,6 +68,11 @@ export const zh: Record = { "startup.unhealthy": "已安装但异常", "startup.conflict": "服务冲突", "startup.installedDisabled": "已安装但禁用", + "startup.install": "安装", + "startup.installing": "正在安装…", + "startup.serviceInstalled": "后台服务安装成功。", + "startup.shimInstalled": "Codex 启动器 shim 安装成功。", + "startup.installFailed": "安装失败:", "startup.tray.title": "Windows 系统托盘", "startup.tray.hint": "登录时启动托盘图标,一键控制代理启动、停止、重启、面板和状态。", "startup.tray.login": "Windows 登录时启动托盘", @@ -84,7 +89,7 @@ export const zh: Record = { "startup.tray.uninstall": "移除登录托盘", "startup.tray.error": "Windows 托盘操作失败。请运行 ocx tray status 查看详情。", "startup.recovery": "修复选项", - "startup.recoveryHint": "在终端运行一个命令。Codex Desktop 和 Windows 可执行文件建议使用后台服务。", + "startup.recoveryHint": "使用上方的一键安装,或复制命令进行手动修复。Codex Desktop 和 Windows 可执行文件建议使用后台服务。", "startup.command.service": "推荐:持久后台服务", "startup.command.shim": "备选:CLI launcher shim", "startup.command.native": "安全恢复:还原 Codex 原生路由", diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index 5231e6e8b36..ef3ba2091b2 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -6,6 +6,7 @@ import { EmptyState } from "../ui"; type StartupStatus = "native" | "protected" | "at-risk"; type StartupProtection = "service" | "shim" | "none"; +type StartupInstallAction = "install-service" | "install-shim"; interface StartupHealthData { status: StartupStatus; @@ -85,6 +86,8 @@ export default function Startup({ apiBase }: { apiBase: string }) { const [trayLoading, setTrayLoading] = useState(true); const [trayBusy, setTrayBusy] = useState(false); const [trayError, setTrayError] = useState(false); + const [installBusy, setInstallBusy] = useState(null); + const [installResult, setInstallResult] = useState<{ kind: "success" | "error"; action: StartupInstallAction; detail?: string } | null>(null); const refresh = useCallback(async (signal?: AbortSignal) => { setLoading(true); @@ -176,6 +179,26 @@ export default function Startup({ apiBase }: { apiBase: string }) { } }; + const runInstallAction = async (action: StartupInstallAction) => { + setInstallBusy(action); + setInstallResult(null); + try { + const res = await fetch(`${apiBase}/api/startup-action`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }), + }); + const body = await res.json().catch(() => null) as { error?: unknown } | null; + if (!res.ok) throw new Error(typeof body?.error === "string" ? body.error : "installation failed"); + setInstallResult({ kind: "success", action }); + await refresh(); + } catch (error) { + setInstallResult({ kind: "error", action, detail: error instanceof Error ? error.message : String(error) }); + } finally { + setInstallBusy(null); + } + }; + const routingKey: TKey = data?.routingKind === "opencodex-local" ? "startup.routing.proxy" : data?.routingKind === "custom-local" ? "startup.routing.customLocal" : data?.routingKind === "custom-remote" ? "startup.routing.customRemote" @@ -247,22 +270,43 @@ export default function Startup({ apiBase }: { apiBase: string }) {
{t("startup.service")}{t("startup.serviceHint")}
- +
+ + {data.serviceSupported && !data.serviceInstalled && ( + + )} +
{t("startup.shim")}{t("startup.shimHint")}
- +
+ + {!data.shimInstalled && ( + + )} +
+ {installResult && ( +
+ {installResult.kind === "success" + ? t(installResult.action === "install-service" ? "startup.serviceInstalled" : "startup.shimInstalled") + : `${t("startup.installFailed")} ${installResult.detail ?? ""}`} +
+ )} {data.platform === "win32" && ( diff --git a/gui/src/styles.css b/gui/src/styles.css index 70f147149d8..f03f2886466 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -734,6 +734,7 @@ dialog.modal-overlay::backdrop { .startup-actions .panel-head > svg { width: 18px; height: 18px; flex: 0 0 auto; } .startup-detail-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid var(--border-soft); } .startup-detail-row > div { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.startup-detail-row > .startup-detail-actions { flex-direction: row; align-items: center; justify-content: flex-end; flex: 0 0 auto; gap: 8px; } .startup-detail-row span:not(.badge) { color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); } .startup-actions > .muted { margin: -4px 0 14px; font-size: var(--text-control); } .startup-tray-buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; } @@ -747,6 +748,8 @@ dialog.modal-overlay::backdrop { @media (max-width: 700px) { .startup-state-grid { grid-template-columns: 1fr; } .startup-command-row { align-items: flex-start; } + .startup-detail-row { align-items: flex-start; } + .startup-detail-row > .startup-detail-actions { flex-direction: column; align-items: flex-end; } } .modal-desc { font-size: var(--text-control); line-height: var(--leading-body); color: var(--muted); margin-bottom: 14px; } .modal-actions { display: flex; gap: 8px; margin-top: 16px; } diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 46fe10e2a0b..e40cb30ff60 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -57,6 +57,7 @@ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, pu import { applySystemEnvToggle } from "../system-env"; import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; import { runWindowsTrayAction } from "../windows-tray-control"; +import { runStartupInstallAction, type StartupInstallAction } from "../startup-action-control"; import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared"; import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; @@ -85,6 +86,22 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise void; @@ -6,6 +7,7 @@ export interface ManagementApiDeps { clearThreadAccountMap?: () => void; clearProviderQuotaCache?: () => void; primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise | void; + runStartupInstallAction?: (action: StartupInstallAction) => Promise<{ message: string }>; } diff --git a/src/server/startup-action-control.ts b/src/server/startup-action-control.ts new file mode 100644 index 00000000000..5cb06532a56 --- /dev/null +++ b/src/server/startup-action-control.ts @@ -0,0 +1,41 @@ +import { execFile } from "node:child_process"; +import { join } from "node:path"; +import { durableBunPath } from "../lib/bun-runtime"; + +export type StartupInstallAction = "install-service" | "install-shim"; +let activeInstall: StartupInstallAction | null = null; + +export function startupInstallArgv(action: StartupInstallAction): string[] { + return action === "install-service" + ? ["service", "install"] + : ["codex-shim", "install"]; +} + +/** Execute the existing fixed CLI installer outside the proxy event loop. */ +export function runStartupInstallAction(action: StartupInstallAction): Promise<{ message: string }> { + if (activeInstall) return Promise.reject(new Error(`Another startup installation is already running: ${activeInstall}`)); + activeInstall = action; + const bun = durableBunPath(); + const cli = join(import.meta.dir, "..", "cli", "index.ts"); + const operation = new Promise<{ message: string }>((resolve, reject) => { + execFile(bun, [cli, ...startupInstallArgv(action)], { + encoding: "utf8", + env: process.env, + timeout: 60_000, + windowsHide: true, + maxBuffer: 256 * 1024, + }, (error, stdout, stderr) => { + if (error) { + const detail = stderr.trim() || stdout.trim() || error.message; + reject(new Error(detail.slice(0, 2_000))); + return; + } + resolve({ + message: action === "install-service" + ? "Background service installed." + : "Codex launcher shim installed.", + }); + }); + }); + return operation.finally(() => { activeInstall = null; }); +} diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index 52ecefe490b..de9d0eed9cc 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -13,7 +13,7 @@ starts the proxy when needed and opens `http://localhost:`. | Endpoint area | Responsibility | | --- | --- | | Config/settings | Read safe config/settings views; mutate supported settings only. Full `PUT /api/config` is disabled so masked secrets are not round-tripped. | -| Startup safety | `GET /api/startup-health` reports whether injected Codex routing is restart-safe, with secret-free service/shim diagnostics. On Windows a healthy script shim is CLI-only; Codex Desktop requires the background service for full protection. | +| Startup safety | `GET /api/startup-health` reports whether injected Codex routing is restart-safe, with secret-free service/shim diagnostics. `POST /api/startup-action` provides allowlisted one-click installation for the background service or launcher shim. On Windows a healthy script shim is CLI-only; Codex Desktop requires the background service for full protection. | | Windows tray | `GET/POST /api/windows-tray` controls an owned, per-user HKCU login tray. The tray delegates fixed actions to the CLI and is never a proxy supervisor or restart-protection signal. | | Providers | Create/update/delete ordinary provider configs and enrich registry metadata. The reserved `openai` card exposes Pool(default)/Direct account mode; `openai-apikey` remains the separate API route. | | Models | Fetch routed model lists, disabled model visibility, and catalog-facing ids. | diff --git a/tests/startup-action-control.test.ts b/tests/startup-action-control.test.ts new file mode 100644 index 00000000000..5c36fd33190 --- /dev/null +++ b/tests/startup-action-control.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test"; +import { startupInstallArgv, type StartupInstallAction } from "../src/server/startup-action-control"; +import { handleManagementAPI } from "../src/server/management-api"; +import type { OcxConfig } from "../src/types"; + +const config = { port: 10100, providers: {}, defaultProvider: "openai", codexAutoStart: true } as OcxConfig; + +describe("startup install actions", () => { + test("maps the allowlisted actions to fixed CLI argv", () => { + expect(startupInstallArgv("install-service")).toEqual(["service", "install"]); + expect(startupInstallArgv("install-shim")).toEqual(["codex-shim", "install"]); + }); + + test("management API dispatches an allowlisted install action", async () => { + const calls: StartupInstallAction[] = []; + const url = new URL("http://localhost/api/startup-action"); + const response = await handleManagementAPI(new Request(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "install-service" }), + }), url, config, { + runStartupInstallAction: async action => { + calls.push(action); + return { message: "installed" }; + }, + }); + expect(response?.status).toBe(200); + expect(await response!.json()).toEqual({ ok: true, action: "install-service", message: "installed" }); + expect(calls).toEqual(["install-service"]); + }); + + test("rejects unknown actions before invoking the installer", async () => { + let called = false; + const url = new URL("http://localhost/api/startup-action"); + const response = await handleManagementAPI(new Request(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "run-anything" }), + }), url, config, { + runStartupInstallAction: async () => { + called = true; + return { message: "unexpected" }; + }, + }); + expect(response?.status).toBe(400); + expect(called).toBe(false); + }); +});