@bitkyc08/opencodex 2.8.0 → 2.8.2-preview.20260731

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/oauth/kiro.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  persistKiroCliSessionRecovery,
19
19
  readImportedKiroCredential,
20
20
  readKiroCliSqliteCredential,
21
+ resolveKiroCliExecutable,
21
22
  restoreKiroCliSession,
22
23
  restoreStaleKiroCliSessionRecovery,
23
24
  requireKiroRegion,
@@ -25,6 +26,7 @@ import {
25
26
  type KiroCliSessionSnapshot,
26
27
  type KiroImportDiagnostic,
27
28
  } from "./kiro-credentials";
29
+ import { homedir } from "node:os";
28
30
  import { getAccountSet, saveAccountCredential } from "./store";
29
31
 
30
32
  const DEFAULT_REGION = "us-east-1";
@@ -72,9 +74,18 @@ const pendingKiroLoginTransactions = new WeakMap<OAuthCredentials, KiroCliSessio
72
74
  /** Forced logins that started with no native CLI DB must logout on persistence failure. */
73
75
  const pendingKiroEmptyPriorSessions = new WeakSet<OAuthCredentials>();
74
76
 
77
+
78
+ function resolveRuntimeKiroCliExecutable(): string {
79
+ return resolveKiroCliExecutable({
80
+ env: process.env,
81
+ platform: process.platform,
82
+ home: process.platform === "win32" ? homedir() : (process.env.HOME || homedir()),
83
+ });
84
+ }
85
+
75
86
  function logoutKiroCliBestEffort(): void {
76
87
  try {
77
- Bun.spawnSync(["kiro-cli", "logout"], {
88
+ Bun.spawnSync([resolveRuntimeKiroCliExecutable(), "logout"], {
78
89
  stdin: "ignore",
79
90
  stdout: "ignore",
80
91
  stderr: "ignore",
@@ -128,7 +139,7 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
128
139
  throwIfKiroLoginCancelled(signal);
129
140
  let child: ReturnType<typeof Bun.spawn>;
130
141
  try {
131
- child = Bun.spawn(["kiro-cli", ...args], {
142
+ child = Bun.spawn([resolveRuntimeKiroCliExecutable(), ...args], {
132
143
  stdin: "ignore",
133
144
  stdout: "pipe",
134
145
  stderr: "ignore",
@@ -18,7 +18,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
18
18
  ],
19
19
  "recurring-credit": ["bytez", "nous-research"],
20
20
  "signup-credit": [
21
- "agentrouter", "ai21", "baichuan", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
21
+ "agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
22
22
  "hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
23
23
  "scaleway", "sensenova", "stepfun", "together", "vertex",
24
24
  ],
@@ -119,6 +119,9 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
119
119
  agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
120
120
  ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
121
121
  baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
122
+ // Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
123
+ // and a chat completion against moonshotai/Kimi-K3 returned a standard chat.completion payload.
124
+ baseten: openAi("https://inference.baseten.co/v1", "https://app.baseten.co/settings/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.baseten.co/inference/model-apis/overview", modelsUrl: "https://inference.baseten.co/v1/models", lastVerified: "2026-07-30" }),
122
125
  deepinfra: openAi("https://api.deepinfra.com/v1/openai", "https://deepinfra.com/dash/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://deepinfra.com/docs/openai_api" }),
123
126
  deepseek: openAi("https://api.deepseek.com", "https://platform.deepseek.com/api_keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://api-docs.deepseek.com/api/list-models" }),
124
127
  doubao: openAi("https://ark.cn-beijing.volces.com/api/v3", "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", { verification: "official" }),
@@ -406,7 +406,7 @@ export function startServer(port?: number) {
406
406
  return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
407
407
  }
408
408
  const goModels = await fetchAllModels(config);
409
- const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } = await import("../codex/catalog");
409
+ const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
410
410
  const nativeSlugs = nativeOpenAiSlugs();
411
411
  const goEnabled = filterCatalogVisibleModels(goModels, config);
412
412
  const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
@@ -424,7 +424,7 @@ export function startServer(port?: number) {
424
424
  if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
425
425
  // Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
426
426
  buildDesktop3pRegistry(
427
- [...visibleNativeSlugs(config)],
427
+ [...desktopVisibleNativeSlugs(config)],
428
428
  goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
429
429
  config.claudeCode?.desktopProfile,
430
430
  );
@@ -441,7 +441,7 @@ export function startServer(port?: number) {
441
441
  : idsParam === "desktop"
442
442
  ? "desktop3p" as const
443
443
  : (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
444
- const data = buildAnthropicModelInfos([...visibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
444
+ const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
445
445
  return jsonResponse({ data }, 200, req, config);
446
446
  }
447
447
  if (url.searchParams.has("client_version")) {
@@ -80,12 +80,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
80
80
  if (config.claudeCode?.desktopAutoApply === false) return;
81
81
  if (!config.claudeCode?.desktopProfile) return;
82
82
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
83
- const { visibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
83
+ const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
84
84
  const allModels = await fetchAllModels(config);
85
85
  const routed = filterCatalogVisibleModels(allModels, config).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
86
86
  const result = writeDesktop3pConfig(
87
87
  config.port ?? 10100,
88
- [...visibleNativeSlugs(config)],
88
+ [...desktopVisibleNativeSlugs(config)],
89
89
  routed,
90
90
  config.apiKeys?.[0]?.key,
91
91
  "static",
@@ -607,7 +607,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
607
607
  config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
608
608
  saveConfigPreservingClaudeCode(config);
609
609
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
610
- const { visibleNativeSlugs } = await import("../../codex/catalog");
610
+ const { desktopVisibleNativeSlugs } = await import("../../codex/catalog");
611
611
  const routed = state.models
612
612
  .filter(model => model.available && !model.route.startsWith("native/"))
613
613
  .map(model => {
@@ -616,7 +616,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
616
616
  });
617
617
  const result = writeDesktop3pConfig(
618
618
  Number(url.port) || config.port,
619
- [...visibleNativeSlugs(config)],
619
+ [...desktopVisibleNativeSlugs(config)],
620
620
  routed,
621
621
  config.apiKeys?.[0]?.key,
622
622
  "static",
@@ -111,6 +111,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
111
111
  );
112
112
  }
113
113
  return jsonResponse({
114
+ // The dashboard renders request-log timestamps. Without this it formats them in the
115
+ // BROWSER's zone, so a KST proxy viewed from a UTC browser reports every request nine
116
+ // hours off (#725). Carried on settings rather than /api/logs because that route's
117
+ // array response has four consumers that would have to change with it.
118
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
114
119
  codexAutoStart: codexAutoStartEnabled(config),
115
120
  port: config.port,
116
121
  hostname: config.hostname ?? "127.0.0.1",
@@ -83,7 +83,7 @@ import { drainAndShutdown } from "../lifecycle";
83
83
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
84
84
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
85
85
  import type { PersistedUsageAttempt } from "../../usage/log";
86
- import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
86
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
87
87
  import { applySystemEnvToggle } from "../system-env";
88
88
 
89
89
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
@@ -98,6 +98,20 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
98
98
  // ~/.opencodex/config.json with the `existing-uuid` test fixture.
99
99
  const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
100
100
 
101
+ if (url.pathname === "/api/catalog" && req.method === "GET") {
102
+ const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
103
+ const catalog = readCatalog(readCodexCatalogPath());
104
+ if (!catalog) return jsonResponse({ error: "catalog not found" }, 404, req, config);
105
+ const headers: Record<string, string> = {
106
+ "Content-Type": "application/json",
107
+ ...corsHeaders(req, config),
108
+ };
109
+ const { loadPersistedCodexRuntime } = await import("../../codex/runtime");
110
+ const version = loadPersistedCodexRuntime()?.selectedVersion;
111
+ if (version) headers["x-opencodex-codex-version"] = version;
112
+ return new Response(JSON.stringify(catalog), { status: 200, headers });
113
+ }
114
+
101
115
  if (url.pathname === "/api/models" && req.method === "GET") {
102
116
  const models = await fetchAllModels(config);
103
117
  const disabled = new Set(config.disabledModels ?? []);
@@ -214,14 +214,14 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
214
214
 
215
215
  /** Shared Desktop profile DTO builder for the management API and CLI. */
216
216
  export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
217
- const { filterCatalogVisibleModels, nativeOpenAiContextWindow, visibleNativeSlugs } = await import("../../codex/catalog");
217
+ const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
218
218
  const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p");
219
219
  const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
220
220
  const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
221
221
  const profileModels: DesktopProfileModel[] = [
222
222
  // Native rows carry their real context window from the same accessor the Grok sync
223
223
  // uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop.
224
- ...visibleNativeSlugs(config).map(id => {
224
+ ...desktopVisibleNativeSlugs(config).map(id => {
225
225
  const contextWindow = nativeOpenAiContextWindow(id);
226
226
  return { route: `native/${id}`, label: `${id} (native)`,
227
227
  ...(contextWindow !== undefined ? { contextWindow } : {}) };
@@ -233,6 +233,19 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
233
233
  })),
234
234
  ];
235
235
  const profile = reconcileDesktopProfile(stored ?? config.claudeCode?.desktopProfile, profileModels);
236
+ if (config.claudeCode?.desktopNativeModels === false) {
237
+ for (const route of Object.keys(profile.assignments)) {
238
+ if (route.startsWith("native/")) delete profile.assignments[route];
239
+ }
240
+ for (const family of ["opus", "fable", "sonnet", "haiku"] as const) {
241
+ const current = profile.defaults[family];
242
+ if (current?.startsWith("native/")) {
243
+ profile.defaults[family] = Object.keys(profile.assignments)
244
+ .filter(route => profile.assignments[route]?.family === family)
245
+ .sort()[0] ?? null;
246
+ }
247
+ }
248
+ }
236
249
  const available = new Set(profileModels.map(model => model.route));
237
250
  const modelByRoute = new Map(profileModels.map(model => [model.route, model]));
238
251
  // Effort support: routed models with a non-empty reasoningEfforts ladder support effort;
@@ -241,7 +254,7 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
241
254
  for (const m of routed) {
242
255
  effortByRoute.set(`${m.provider}/${m.id}`, Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0);
243
256
  }
244
- for (const id of visibleNativeSlugs(config)) {
257
+ for (const id of desktopVisibleNativeSlugs(config)) {
245
258
  effortByRoute.set(`native/${id}`, true);
246
259
  }
247
260
  const models = Object.keys(profile.assignments).sort().map(route => ({
@@ -109,6 +109,13 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
109
109
  return verified === candidate ? verified : null;
110
110
  };
111
111
 
112
+ const verifiedReportedPid = (reported: number | null): number | null => {
113
+ if (reported === null) return null;
114
+ if (!Number.isSafeInteger(reported) || reported <= 0) return null;
115
+ const verified = verifyPidFn(reported);
116
+ return verified === reported ? verified : null;
117
+ };
118
+
112
119
  const pid = readPidFn();
113
120
  let probedPort: number | null = null;
114
121
  if (pid) {
@@ -136,7 +143,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
136
143
  // (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
137
144
  // would hand destructive callers (stopProxy → kill fallback) a reusable pid.
138
145
  if (identity) {
139
- return { pid: identity.pid ?? null, port: record.port, hostname: record.hostname, source: "runtime" };
146
+ return { pid: verifiedReportedPid(identity.pid), port: record.port, hostname: record.hostname, source: "runtime" };
140
147
  }
141
148
  }
142
149
 
@@ -145,7 +152,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
145
152
  const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
146
153
  if (identity) {
147
154
  return {
148
- pid: identity.pid ?? killablePid(pid),
155
+ pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
149
156
  port,
150
157
  hostname: config.hostname,
151
158
  source: "config",
package/src/service.ts CHANGED
@@ -6,10 +6,11 @@
6
6
  * restore it via the command.
7
7
  */
8
8
  import { execFileSync, execSync } from "node:child_process";
9
+ import { findLiveProxy } from "./server/proxy-liveness";
9
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
11
  import { homedir } from "node:os";
11
12
  import { dirname, join, resolve } from "node:path";
12
- import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
13
+ import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
13
14
  import { loadConfig } from "./config";
14
15
  import { restoreNativeCodex } from "./codex/inject";
15
16
  import { stripGrokConfig } from "./grok/inject";
@@ -17,7 +18,6 @@ import { isWslRuntime } from "./codex/home";
17
18
  import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
18
19
  import { isProcessAlive, stopProxy } from "./lib/process-control";
19
20
  import { serviceApiTokenFilePath } from "./lib/service-secrets";
20
- import { findLiveProxy } from "./server/proxy-liveness";
21
21
  import { randomUUID } from "node:crypto";
22
22
  import {
23
23
  ELEVATION_REQUEST_TIMEOUT_MS,
@@ -1428,7 +1428,38 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
1428
1428
  * scheduler backend first; on failure the machine is left with NO service (explicitly
1429
1429
  * reported) — never a silent fallback to the scheduler.
1430
1430
  */
1431
+ /** Refuse WinSW when the interactive user is a Microsoft account (SCM cannot authenticate it). */
1432
+ export function assertWindowsNativeServiceAccountSupported(): void {
1433
+ if (process.platform !== "win32") return;
1434
+ const source = readWindowsPrincipalSource();
1435
+ if (source?.toLowerCase() === "microsoftaccount") {
1436
+ throw new Error(
1437
+ "The native (WinSW) service backend cannot run under a Microsoft-account Windows login. "
1438
+ + "Keep the Task Scheduler backend (`ocx service install`) or sign in with a local/domain account before `ocx service install --native`.",
1439
+ );
1440
+ }
1441
+ }
1442
+
1443
+ function readWindowsPrincipalSource(): string | null {
1444
+ if (process.platform !== "win32") return null;
1445
+ const ps = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
1446
+ if (!existsSync(ps)) return null;
1447
+ try {
1448
+ const out = execFileSync(ps, [
1449
+ "-NoLogo",
1450
+ "-NoProfile",
1451
+ "-NonInteractive",
1452
+ "-Command",
1453
+ "(Get-LocalUser -Name $env:USERNAME -ErrorAction SilentlyContinue).PrincipalSource",
1454
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
1455
+ return out || null;
1456
+ } catch {
1457
+ return null;
1458
+ }
1459
+ }
1460
+
1431
1461
  async function installWindowsNative(): Promise<void> {
1462
+ assertWindowsNativeServiceAccountSupported();
1432
1463
  recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1433
1464
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
1434
1465
  writeServiceApiTokenFile();
@@ -1463,11 +1494,49 @@ async function installWindowsNative(): Promise<void> {
1463
1494
  writeServiceInstallState("native");
1464
1495
  }
1465
1496
  function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
1466
- function stopWindows(): void { try { schtasks(["/end", "/tn", TASK]); } catch { /* not running */ } }
1497
+
1498
+ export function isWindowsSchedulerEndBenign(error: unknown): boolean {
1499
+ const detail = schtasksErrorDetail(error).toLowerCase();
1500
+ return detail.includes("no running instance")
1501
+ || detail.includes("not currently running")
1502
+ || detail.includes("0x41330");
1503
+ }
1504
+
1505
+ /**
1506
+ * End the scheduler task. "Already stopped" is success; other `/end` failures are
1507
+ * swallowed so callers can still run tracked-proxy + live-proxy cleanup.
1508
+ *
1509
+ * Do not key a restart-window wait on `/end` failure: the #764 case is an `/end`
1510
+ * that *succeeds* while the wrapper survives and respawns. That verification lives
1511
+ * on the stop-verification path (poll across the restart window), not here.
1512
+ */
1513
+ export function stopWindows(): void {
1514
+ try {
1515
+ schtasks(["/end", "/tn", TASK]);
1516
+ } catch (error) {
1517
+ if (isWindowsSchedulerEndBenign(error)) return;
1518
+ }
1519
+ }
1467
1520
  function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
1468
1521
  function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
1469
1522
  function uninstallWindows(): void {
1470
- try { schtasks(["/delete", "/tn", TASK, "/f"]); } catch { /* absent */ }
1523
+ const probe = probeWindowsSchedulerTask(TASK);
1524
+ if (probe.status === "present") {
1525
+ try {
1526
+ schtasks(["/delete", "/tn", TASK, "/f"]);
1527
+ } catch (error) {
1528
+ throw new Error(`Failed to delete Task Scheduler task ${TASK}: ${error instanceof Error ? error.message : String(error)}`);
1529
+ }
1530
+ const afterDelete = probeWindowsSchedulerTask(TASK);
1531
+ if (afterDelete.status === "present") {
1532
+ throw new Error(`Task Scheduler task ${TASK} is still present after delete — refusing to remove service assets. Retry from an elevated shell.`);
1533
+ }
1534
+ if (afterDelete.status === "unknown") {
1535
+ throw new Error(`Task Scheduler task ${TASK} presence could not be verified after delete — refusing to remove service assets.`);
1536
+ }
1537
+ } else if (probe.status === "unknown") {
1538
+ throw new Error(`Task Scheduler task ${TASK} presence could not be verified — refusing to remove service assets.`);
1539
+ }
1471
1540
  if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
1472
1541
  if (existsSync(windowsLauncherVbsPath())) unlinkSync(windowsLauncherVbsPath());
1473
1542
  if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
@@ -1626,18 +1695,77 @@ function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
1626
1695
 
1627
1696
  type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
1628
1697
 
1698
+ function verifiedKillTarget(pid: number | null | undefined): number | null {
1699
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
1700
+ const verified = verifyPidIdentity(pid);
1701
+ return verified === pid ? verified : null;
1702
+ }
1703
+
1704
+ /**
1705
+ * Whether a proxy is still answering after the service manager claimed to stop it.
1706
+ *
1707
+ * `ops.stop()` reports the outcome of the STOP COMMAND, not of the process. A Windows scheduler
1708
+ * task whose wrapper survives `schtasks /end` respawns its child a few seconds later, so a stop
1709
+ * that returned success can still leave a live proxy — and `ocx service stop` then restored
1710
+ * native Codex on top of a running one (#764). The tracked-pid cleanup does not catch it either:
1711
+ * the respawned child writes a different pid, or none this process knows about.
1712
+ *
1713
+ * Probed rather than assumed, and bounded. The respawn risk is specific to a supervisor that can
1714
+ * restart its child — the Windows scheduler wrapper — so only that case pays the restart window.
1715
+ * Everywhere else a single probe answers the question, because nothing is going to bring the
1716
+ * proxy back after `launchctl unload` or `systemctl stop`. Making every platform wait 7s on a
1717
+ * stop that already succeeded would trade one bug for a worse everyday one.
1718
+ */
1719
+ export async function proxyStillLiveAfterStop(deps: {
1720
+ findProxy?: () => Promise<{ port: number } | null>;
1721
+ sleep?: (ms: number) => Promise<void>;
1722
+ now?: () => number;
1723
+ /** Whether the stopped supervisor can respawn its child; only then is polling worth the wait. */
1724
+ canRespawn?: boolean;
1725
+ } = {}): Promise<{ port: number } | null> {
1726
+ const findProxy = deps.findProxy ?? findLiveProxy;
1727
+ const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
1728
+ const now = deps.now ?? Date.now;
1729
+ const canRespawn = deps.canRespawn ?? process.platform === "win32";
1730
+ const deadline = now() + (canRespawn ? 7000 : 0);
1731
+ for (;;) {
1732
+ try {
1733
+ const live = await findProxy();
1734
+ if (live) return live;
1735
+ } catch {
1736
+ // A probe failure is not proof the proxy is gone; keep polling until the deadline.
1737
+ }
1738
+ if (now() >= deadline) return null;
1739
+ await sleep(1000);
1740
+ }
1741
+ }
1742
+
1629
1743
  async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
1744
+ let stopped = false;
1630
1745
  const pid = readPid();
1631
- if (!pid) return "none";
1632
- if (!isProcessAlive(pid)) {
1746
+ const trackedKillPid = verifiedKillTarget(pid);
1747
+ if (trackedKillPid !== null && isProcessAlive(trackedKillPid)) {
1748
+ await stopProxy(trackedKillPid);
1749
+ removePid(trackedKillPid);
1750
+ removeRuntimePort(trackedKillPid);
1751
+ stopped = true;
1752
+ } else if (pid) {
1633
1753
  removePid(pid);
1634
1754
  removeRuntimePort(pid);
1635
- return "stale";
1636
1755
  }
1637
- await stopProxy(pid);
1638
- removePid(pid);
1639
- removeRuntimePort(pid);
1640
- return "stopped";
1756
+ // Orphan recovery: the pid file can be missing/stale while the service wrapper keeps
1757
+ // a live proxy running — mirror `ocx stop`'s identity-checked findLiveProxy fallback.
1758
+ const live = await findLiveProxy({ timeoutMs: 1500 });
1759
+ const liveKillPid = verifiedKillTarget(live?.pid);
1760
+ if (liveKillPid !== null) {
1761
+ await stopProxy(liveKillPid);
1762
+ removePid(liveKillPid);
1763
+ removeRuntimePort(liveKillPid);
1764
+ stopped = true;
1765
+ }
1766
+ if (stopped) return "stopped";
1767
+ if (pid) return "stale";
1768
+ return "none";
1641
1769
  }
1642
1770
 
1643
1771
  async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupResult> {
@@ -1945,13 +2073,29 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1945
2073
  ops.start();
1946
2074
  console.log("✅ service started.");
1947
2075
  break;
1948
- case "stop":
2076
+ case "stop": {
1949
2077
  assertServiceEnvironmentMatchesInstall();
1950
2078
  // Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
1951
2079
  // (and its Windows/Linux twins) even with nothing installed.
1952
- if (ops.status() !== null || isServiceInstalled()) ops.stop();
2080
+ if (ops.status() !== null || isServiceInstalled()) {
2081
+ ops.stop();
2082
+ }
1953
2083
  await stopTrackedProxyForServiceCommand();
1954
2084
  {
2085
+ // Verify rather than trust the stop command: a surviving wrapper respawns its child
2086
+ // seconds later, and restoring native Codex on top of a live proxy is the failure #764
2087
+ // reports as "stop reports success without stopping the proxy".
2088
+ const survivor = await proxyStillLiveAfterStop();
2089
+ if (survivor) {
2090
+ console.error(
2091
+ `❌ service stop did not take effect: a proxy is still listening on port ${survivor.port}.`
2092
+ + "\nNative Codex was NOT restored, because doing so while the proxy is running leaves"
2093
+ + " both pointing at each other. Check for a second service backend (`ocx service status`)"
2094
+ + " or a manually started proxy, then re-run `ocx service stop`.",
2095
+ );
2096
+ process.exitCode = 1;
2097
+ break;
2098
+ }
1955
2099
  const restore = restoreNativeCodex();
1956
2100
  if (restore.success) console.log("✅ service stopped + native Codex restored.");
1957
2101
  else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
@@ -1962,6 +2106,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
1962
2106
  else if (!grok.ok) console.error(`⚠️ ${grok.message}`);
1963
2107
  }
1964
2108
  break;
2109
+ }
1965
2110
  case "status": {
1966
2111
  if (process.platform === "win32" && backend === "scheduler") {
1967
2112
  console.log(await inspectWindowsSchedulerServiceStatus());
@@ -2007,3 +2152,4 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2007
2152
  process.exit(1);
2008
2153
  }
2009
2154
  }
2155
+
@@ -150,8 +150,16 @@ function quoteRunValue(value: string): string {
150
150
  return `\"${value}\"`;
151
151
  }
152
152
 
153
- /** Command persisted under HKCU Run. Every value is an owned absolute package/home path. */
154
- export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
153
+ function installedTrayLauncherPath(): string {
154
+ return join(getConfigDir(), "opencodex-tray.vbs");
155
+ }
156
+
157
+ function quoteVbsPath(value: string): string {
158
+ return value.replace(/"/g, '""');
159
+ }
160
+
161
+ /** Full PowerShell invocation used by the owned VBS launcher (not written to HKCU Run). */
162
+ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
155
163
  return [
156
164
  quoteRunValue(powershell),
157
165
  "-NoLogo",
@@ -169,6 +177,27 @@ export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry, powershell =
169
177
  ].join(" ");
170
178
  }
171
179
 
180
+ /** Short HKCU Run command (must stay ≤260 chars under long Windows user/npm paths). */
181
+ export function buildWindowsTrayRunCommand(entry: WindowsTrayEntry & { launcherPath: string }): string {
182
+ const wscript = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "wscript.exe");
183
+ return `${quoteRunValue(wscript)} //B //NoLogo ${quoteRunValue(entry.launcherPath)}`;
184
+ }
185
+
186
+ export function buildWindowsTrayLauncherScript(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
187
+ const command = buildWindowsTrayPowerShellCommand(entry, powershell);
188
+ // VBS CreateObject("WScript.Shell").Run command, 0, False — hidden, non-blocking.
189
+ return [
190
+ "' OpenCodex owned tray launcher — do not edit by hand.",
191
+ `CreateObject("WScript.Shell").Run "${quoteVbsPath(command)}", 0, False`,
192
+ "",
193
+ ].join("\r\n");
194
+ }
195
+
196
+ /** @deprecated Prefer buildWindowsTrayPowerShellCommand; kept for callers that still expect the long form. */
197
+ export function buildWindowsTrayLegacyRunCommand(entry: WindowsTrayEntry, powershell = windowsPowerShellPath()): string {
198
+ return buildWindowsTrayPowerShellCommand(entry, powershell);
199
+ }
200
+
172
201
  function readState(): WindowsTrayState | null {
173
202
  try {
174
203
  const state = JSON.parse(readFileSync(trayStatePath(), "utf8")) as Partial<WindowsTrayState>;
@@ -204,7 +233,11 @@ function replaceOwnedFile(path: string, contents: string | Buffer): void {
204
233
  }
205
234
  }
206
235
 
207
- function writeState(entry: WindowsTrayEntry, runValue: string, runCommand: string): void {
236
+ function writeState(
237
+ entry: WindowsTrayEntry & { launcherPath: string },
238
+ runValue: string,
239
+ runCommand: string,
240
+ ): void {
208
241
  const path = trayStatePath();
209
242
  replaceOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
210
243
  }
@@ -376,7 +409,8 @@ function trayStatusFrom(registered: string | null): WindowsTrayStatus {
376
409
  const running = heartbeatProcessAlive(heartbeat);
377
410
  const registrationOwned = state !== null
378
411
  && registered === state.runCommand
379
- && [state.bun, state.cli, state.script, ...installedTrayIconPaths()].every(path => existsSync(path));
412
+ && [state.bun, state.cli, state.script, ...(state.launcherPath ? [state.launcherPath] : []), ...installedTrayIconPaths()]
413
+ .every(path => existsSync(path));
380
414
  const stale = windowsTrayRegistrationIsStale({
381
415
  registered: registered !== null,
382
416
  registrationOwned,
@@ -507,7 +541,12 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
507
541
  }
508
542
  recordOwnedConfigPath(getConfigDir(), trayStatePath());
509
543
  if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
510
- const runCommand = buildWindowsTrayRunCommand(entry);
544
+ const launcherPath = installedTrayLauncherPath();
545
+ const entryWithLauncher = { ...entry, launcherPath };
546
+ const runCommand = buildWindowsTrayRunCommand(entryWithLauncher);
547
+ if (runCommand.length > 260) {
548
+ throw new Error(`Tray Run command exceeds the Windows 260-character limit (${runCommand.length} chars).`);
549
+ }
511
550
  const runValue = windowsTrayRunValue(entry.opencodexHome);
512
551
  const existing = readOwnedRunValue(runValue);
513
552
  const state = readState();
@@ -517,6 +556,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
517
556
  if (existsSync(entry.script) && (!state || resolve(state.script) !== resolve(entry.script))) {
518
557
  throw new Error(`Refusing to overwrite an unowned tray script at ${entry.script}.`);
519
558
  }
559
+ if (existsSync(launcherPath) && (!state?.launcherPath || resolve(state.launcherPath) !== resolve(launcherPath))) {
560
+ throw new Error(`Refusing to overwrite an unowned tray launcher at ${launcherPath}.`);
561
+ }
520
562
  if (!state && iconPairs.some(pair => existsSync(pair.installed))) {
521
563
  throw new Error("Refusing to overwrite unowned Windows tray icon assets.");
522
564
  }
@@ -531,6 +573,7 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
531
573
 
532
574
  const previousStateBytes = existsSync(trayStatePath()) ? readFileSync(trayStatePath()) : null;
533
575
  const previousScriptBytes = existsSync(entry.script) ? readFileSync(entry.script) : null;
576
+ const previousLauncherBytes = existsSync(launcherPath) ? readFileSync(launcherPath) : null;
534
577
  const previousIconBytes = new Map(iconPairs.map(pair => [
535
578
  pair.installed,
536
579
  existsSync(pair.installed) ? readFileSync(pair.installed) : null,
@@ -540,6 +583,10 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
540
583
  if (previousScriptBytes) replaceOwnedFile(entry.script, previousScriptBytes);
541
584
  else if (existsSync(entry.script)) unlinkSync(entry.script);
542
585
  } catch { /* rollback best-effort */ }
586
+ try {
587
+ if (previousLauncherBytes) replaceOwnedFile(launcherPath, previousLauncherBytes);
588
+ else if (existsSync(launcherPath)) unlinkSync(launcherPath);
589
+ } catch { /* rollback best-effort */ }
543
590
  for (const [path, contents] of previousIconBytes) {
544
591
  try {
545
592
  if (contents) replaceOwnedFile(path, contents);
@@ -567,8 +614,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
567
614
  if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
568
615
  replaceOwnedFile(entry.script, readFileSync(sourceScript));
569
616
  for (const pair of iconPairs) replaceOwnedFile(pair.installed, readFileSync(pair.source));
617
+ replaceOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
570
618
  runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
571
- writeState(entry, runValue, runCommand);
619
+ writeState(entryWithLauncher, runValue, runCommand);
572
620
  } catch (error) {
573
621
  restorePreviousInstall();
574
622
  throw error;
@@ -578,9 +626,6 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
578
626
  restorePreviousInstall();
579
627
  throw new Error("The tray startup registration was installed, but the tray process did not become healthy.");
580
628
  }
581
- if (state?.launcherPath && existsSync(state.launcherPath)) {
582
- try { unlinkSync(state.launcherPath); } catch { /* old owned VBS is inert after a committed Run replacement */ }
583
- }
584
629
  return getWindowsTrayStatus();
585
630
  }
586
631
 
package/src/types.ts CHANGED
@@ -455,6 +455,11 @@ export interface OcxClaudeCodeConfig {
455
455
  desktopProfile?: OcxClaudeDesktopProfile;
456
456
  /** Auto-reconcile Desktop 3P config when provider catalog changes. Default: enabled. */
457
457
  desktopAutoApply?: boolean;
458
+ /**
459
+ * When false, omit `native/*` rows from Claude Desktop show/export/apply. Default: enabled.
460
+ * Routing-sidecar alias decoding is unchanged — only the Desktop model list writer.
461
+ */
462
+ desktopNativeModels?: boolean;
458
463
  }
459
464
 
460
465
  export type OcxClaudeDesktopFamily = "opus" | "fable" | "sonnet" | "haiku";