@bitkyc08/opencodex 2.75.0 → 2.76.0

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.
Files changed (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
package/src/combos/jev.ts CHANGED
@@ -3,21 +3,38 @@ import {
3
3
  providerOutboundPost,
4
4
  providerRedirectError,
5
5
  } from "../lib/provider-outbound";
6
- import { resolveProviderApiKey } from "../providers/api-key-resolve";
6
+ import {
7
+ isKeychainReference,
8
+ keychainReferenceBelongsToProvider,
9
+ resolveProviderApiKey,
10
+ } from "../providers/api-key-resolve";
7
11
  import { providerMatchesRegistryTransport } from "../providers/registry";
8
12
  import type { OcxComboDefaultEffort, OcxConfig, OcxProviderConfig } from "../types";
9
- import { JEV_MAX_CANDIDATE_FIELD_CHARS } from "./types";
13
+ import {
14
+ isSystemOneEndpoint,
15
+ JEV_DECISION_TIMEOUT_DEFAULT_MS,
16
+ JEV_DECISION_TIMEOUT_MAX_MS,
17
+ JEV_DECISION_TIMEOUT_MIN_MS,
18
+ JEV_MAX_CANDIDATE_FIELD_CHARS,
19
+ } from "./types";
10
20
 
11
21
  export const JEV_PROVIDER_ID = "jev";
12
22
  export const JEV_API_URL = "https://api.typesafe.ai/v1/systemone";
13
23
  export const JEV_MODEL = "jev-latest";
14
24
 
15
- const JEV_TIMEOUT_MS = 4_000;
16
- const JEV_MAX_CANDIDATES = 64;
17
- const JEV_MAX_REQUEST_BYTES = 65_536;
18
- const JEV_MAX_RESPONSE_BYTES = 65_536;
25
+ export const JEV_MAX_CANDIDATES = 64;
26
+ /** Ollama's System One choice questions accept 2..26 options; other services share the floor. */
27
+ const SELF_HOSTED_MIN_OPTIONS = 2;
28
+ const SELF_HOSTED_MAX_OPTIONS = 26;
29
+ /** Environment names that hold TypeSafe credentials; a self-hosted row may never reference them. */
30
+ const TYPESAFE_ENV_KEYS = new Set(["TYPESAFE_API_KEY", "JEV_API_KEY"]);
31
+ export const JEV_MAX_REQUEST_BYTES = 65_536;
32
+ export const JEV_MAX_RESPONSE_BYTES = 65_536;
19
33
  const JEV_OUTBOUND_DEPENDENCIES = {
20
34
  isCanonicalUrl: (name: string, url: string) => name === JEV_PROVIDER_ID && url === JEV_API_URL,
35
+ // Self-hosted decision models (Ollama tev1) listen on plain HTTP loopback. The outbound wrapper
36
+ // still requires the row's own allowPrivateNetwork and a literal local address for that.
37
+ allowLocalCleartextPost: true,
21
38
  };
22
39
 
23
40
  const TASK_CHARS = 500;
@@ -74,7 +91,23 @@ export interface JevCandidate {
74
91
  modelProfile?: string;
75
92
  }
76
93
 
94
+ /**
95
+ * Which kind of service answered a decision. `typesafe` is the canonical System One endpoint,
96
+ * `systemone` a configured System One-compatible row, `model` an opencodex-routed model.
97
+ */
98
+ export type JevDecisionBackend = "typesafe" | "systemone" | "model";
99
+
100
+ /** The backend a combo's decision settings select. A decision model wins over a provider row. */
101
+ export function jevDecisionBackendFor(
102
+ combo: { decisionProvider?: string | null; decisionModel?: string | null },
103
+ ): JevDecisionBackend {
104
+ if (typeof combo.decisionModel === "string" && combo.decisionModel.trim()) return "model";
105
+ const provider = typeof combo.decisionProvider === "string" ? combo.decisionProvider.trim() : "";
106
+ return provider && provider !== JEV_PROVIDER_ID ? "systemone" : "typesafe";
107
+ }
108
+
77
109
  export interface JevDecision {
110
+ backend: JevDecisionBackend;
78
111
  targetKey: string;
79
112
  effort: OcxComboDefaultEffort | null;
80
113
  gate: "apply" | "missing_key" | "no_choices" | "no_state" | "timeout" | "network" | "redirect" | "http" | "malformed" | "invalid";
@@ -89,6 +122,13 @@ export interface ResolveJevDecisionOptions {
89
122
  candidates: readonly JevCandidate[];
90
123
  fallback: { targetKey: string; effort: OcxComboDefaultEffort | null };
91
124
  config: OcxConfig;
125
+ /**
126
+ * Decision service provider id. Omitted or `"jev"` keeps the canonical TypeSafe destination;
127
+ * any other id names a configured `jev-decision` row (for example a self-hosted Ollama `tev1`).
128
+ */
129
+ decisionProvider?: string;
130
+ /** Decision deadline; values outside 1000..120000 ms keep the four-second default. */
131
+ timeoutMs?: number;
92
132
  signal?: AbortSignal;
93
133
  post?: typeof providerOutboundPost;
94
134
  now?: () => number;
@@ -400,7 +440,7 @@ export function buildJevState(body: unknown, candidates: readonly JevCandidate[]
400
440
  };
401
441
  }
402
442
 
403
- function hasJevDecisionState(state: Record<string, unknown>): boolean {
443
+ export function hasJevDecisionState(state: Record<string, unknown>): boolean {
404
444
  if (typeof state.task === "string" && state.task.trim()) return true;
405
445
  if (isRecord(state.signals) && state.signals.has_image === true) return true;
406
446
  return isRecord(state.step)
@@ -431,7 +471,7 @@ function candidateOptions(candidates: readonly JevCandidate[]): Map<string, JevR
431
471
  return options;
432
472
  }
433
473
 
434
- function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
474
+ export function candidatesFitRequestBounds(candidates: readonly JevCandidate[]): boolean {
435
475
  if (candidates.length > JEV_MAX_CANDIDATES) return false;
436
476
  return candidates.every(candidate => [candidate.key, candidate.provider, candidate.model]
437
477
  .every(value => value.length > 0 && value.length <= JEV_MAX_CANDIDATE_FIELD_CHARS)
@@ -445,10 +485,47 @@ function modelProfile(candidate: JevCandidate): string {
445
485
  ?? "Configured target with capability unspecified by JEV; judge it only from the supplied request evidence.";
446
486
  }
447
487
 
448
- export function buildJevRouteQuestion(candidates: readonly JevCandidate[]): Record<string, unknown> {
449
- const options = candidateOptions(candidates);
488
+ function criterionDescription(criterion: JevRouteOption["criterion"]): string {
489
+ const effort = criterion.reasoning_effort
490
+ ? `${criterion.reasoning_effort} reasoning effort`
491
+ : "no reasoning-effort control";
492
+ return `Target ${criterion.target} (provider ${criterion.provider}, model ${criterion.model}) with ${effort}.`;
493
+ }
494
+
495
+ /** One allowlisted target/effort option, shared by every decision backend. */
496
+ export interface JevRouteOptionDescriptor {
497
+ key: string;
498
+ targetKey: string;
499
+ effort: OcxComboDefaultEffort | null;
500
+ description: string;
501
+ }
502
+
503
+ /** Every allowlisted option in question order; throws on a duplicate choice like the builder. */
504
+ export function jevRouteOptions(candidates: readonly JevCandidate[]): JevRouteOptionDescriptor[] {
505
+ return [...candidateOptions(candidates)].map(([key, option]) => ({
506
+ key,
507
+ targetKey: option.targetKey,
508
+ effort: option.effort,
509
+ description: criterionDescription(option.criterion),
510
+ }));
511
+ }
512
+
513
+ /**
514
+ * Build the single `route` choice question.
515
+ *
516
+ * TypeSafe receives each option's structured criterion object. `descriptiveCriteria` renders the
517
+ * same facts as one plain description string per option instead, the portable System One shape
518
+ * that self-hosted services such as Ollama require ("option keys to descriptions or null").
519
+ */
520
+ export function buildJevRouteQuestion(
521
+ candidates: readonly JevCandidate[],
522
+ options: { descriptiveCriteria?: boolean } = {},
523
+ ): Record<string, unknown> {
524
+ const routeOptions = candidateOptions(candidates);
450
525
  const criteria: Record<string, unknown> = {};
451
- for (const [choice, option] of options) criteria[choice] = option.criterion;
526
+ for (const [choice, option] of routeOptions) {
527
+ criteria[choice] = options.descriptiveCriteria ? criterionDescription(option.criterion) : option.criterion;
528
+ }
452
529
  const modelProfiles: Record<string, string> = {};
453
530
  for (const candidate of candidates) modelProfiles[candidate.key] = modelProfile(candidate);
454
531
  return {
@@ -468,7 +545,7 @@ export function buildJevRouteQuestion(candidates: readonly JevCandidate[]): Reco
468
545
  };
469
546
  }
470
547
 
471
- function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
548
+ export function jevUsage(payload: Record<string, unknown>): Record<string, number> | undefined {
472
549
  if (!isRecord(payload.usage)) return undefined;
473
550
  const usage: Record<string, number> = {};
474
551
  for (const [key, value] of Object.entries(payload.usage)) {
@@ -530,12 +607,23 @@ export function parseJevDecision(
530
607
  };
531
608
  }
532
609
 
533
- function fallbackDecision(
610
+ export function fallbackDecision(
534
611
  fallback: ResolveJevDecisionOptions["fallback"],
535
612
  gate: Exclude<JevDecision["gate"], "apply">,
536
613
  latencyMs: number,
614
+ backend: JevDecisionBackend,
537
615
  ): JevDecision {
538
- return { ...fallback, gate, latencyMs };
616
+ return { backend, ...fallback, gate, latencyMs };
617
+ }
618
+
619
+ /** The decision deadline: the configured value when in bounds, otherwise the four-second default. */
620
+ export function jevDecisionTimeoutMs(value: number | undefined): number {
621
+ return value !== undefined
622
+ && Number.isInteger(value)
623
+ && value >= JEV_DECISION_TIMEOUT_MIN_MS
624
+ && value <= JEV_DECISION_TIMEOUT_MAX_MS
625
+ ? value
626
+ : JEV_DECISION_TIMEOUT_DEFAULT_MS;
539
627
  }
540
628
 
541
629
  function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
@@ -549,8 +637,87 @@ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
549
637
  };
550
638
  }
551
639
 
640
+ interface JevDecisionEndpoint {
641
+ name: string;
642
+ provider: OcxProviderConfig;
643
+ url: string;
644
+ model: string;
645
+ apiKey: string | undefined;
646
+ /** Self-hosted System One services accept only string option descriptions. */
647
+ descriptiveCriteria: boolean;
648
+ }
649
+
650
+ function envReferenceName(value: string): string | undefined {
651
+ const braced = /^\$\{(\w+)\}$/.exec(value);
652
+ if (braced) return braced[1];
653
+ return value.startsWith("$") ? value.slice(1) : undefined;
654
+ }
655
+
656
+ /**
657
+ * A self-hosted row may carry only its own secret: never a reference to the TypeSafe environment
658
+ * keys, and never a keychain entry that belongs to another provider. `null` means refused.
659
+ */
660
+ function selfHostedApiKey(name: string, apiKey: string | undefined): string | undefined | null {
661
+ if (!apiKey) return undefined;
662
+ const envName = envReferenceName(apiKey);
663
+ if (envName !== undefined && TYPESAFE_ENV_KEYS.has(envName)) return null;
664
+ if (isKeychainReference(apiKey) && !keychainReferenceBelongsToProvider(apiKey, name)) return null;
665
+ return resolveProviderApiKey(apiKey)?.trim() || undefined;
666
+ }
667
+
668
+ /**
669
+ * Resolve where one decision request goes and which credential it may carry.
670
+ *
671
+ * The `jev` id stays pinned to the canonical TypeSafe URL and `jev-latest`: its row key is used
672
+ * only while the row still matches the registry transport, and the environment fallbacks exist
673
+ * only for that URL. A retargeted `jev` row therefore keeps today's behavior instead of becoming a
674
+ * custom destination. Any other id must be an enabled `jev-decision` row whose baseUrl is a
675
+ * `/systemone` endpoint and which names its own model; only its own key may accompany it, so no
676
+ * TypeSafe credential can reach a self-hosted service. `undefined` means no usable decision
677
+ * service (reported through the existing `missing_key` gate).
678
+ */
679
+ function jevDecisionEndpoint(config: OcxConfig, decisionProvider: string): JevDecisionEndpoint | undefined {
680
+ const configured = Object.hasOwn(config.providers, decisionProvider)
681
+ ? config.providers[decisionProvider]
682
+ : undefined;
683
+ if (configured?.disabled === true) return undefined;
684
+ if (decisionProvider === JEV_PROVIDER_ID) {
685
+ const configuredOwnsJev = configured
686
+ && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
687
+ const apiKey = (
688
+ configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
689
+ ) || process.env.TYPESAFE_API_KEY?.trim()
690
+ || process.env.JEV_API_KEY?.trim();
691
+ if (!apiKey) return undefined;
692
+ return {
693
+ name: JEV_PROVIDER_ID,
694
+ provider: canonicalJevProvider(config),
695
+ url: JEV_API_URL,
696
+ model: JEV_MODEL,
697
+ apiKey,
698
+ descriptiveCriteria: false,
699
+ };
700
+ }
701
+ if (configured?.adapter !== "jev-decision" || typeof configured.baseUrl !== "string") return undefined;
702
+ const url = configured.baseUrl.trim().replace(/\/+$/, "");
703
+ if (!url || !isSystemOneEndpoint(url)) return undefined;
704
+ // `jev-latest` is TypeSafe's model name; a self-hosted host must name its own.
705
+ const model = configured.defaultModel?.trim() || configured.models?.[0]?.trim();
706
+ if (!model) return undefined;
707
+ const apiKey = selfHostedApiKey(decisionProvider, configured.apiKey);
708
+ if (apiKey === null) return undefined;
709
+ return {
710
+ name: decisionProvider,
711
+ provider: configured,
712
+ url,
713
+ model,
714
+ apiKey,
715
+ descriptiveCriteria: true,
716
+ };
717
+ }
718
+
552
719
  /**
553
- * Ask TypeSafe JEV for one allowlisted target/effort decision.
720
+ * Ask the configured JEV decision service for one allowlisted target/effort decision.
554
721
  *
555
722
  * Every operational or response failure returns the supplied first-eligible fallback. A caller
556
723
  * abort is the exception: request cancellation remains cancellation and is rethrown by identity.
@@ -558,38 +725,40 @@ function canonicalJevProvider(config: OcxConfig): OcxProviderConfig {
558
725
  export async function resolveJevDecision(options: ResolveJevDecisionOptions): Promise<JevDecision> {
559
726
  const now = options.now ?? Date.now;
560
727
  const startedAt = now();
728
+ const backend = jevDecisionBackendFor({ decisionProvider: options.decisionProvider });
561
729
  const failed = (gate: Exclude<JevDecision["gate"], "apply">): JevDecision =>
562
- fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt));
730
+ fallbackDecision(options.fallback, gate, Math.max(0, now() - startedAt), backend);
563
731
 
564
732
  if (options.signal?.aborted) throw options.signal.reason;
565
733
  if (options.candidates.length === 0) return failed("no_choices");
566
734
  if (!candidatesFitRequestBounds(options.candidates)) return failed("invalid");
567
735
 
568
- const configured = options.config.providers[JEV_PROVIDER_ID];
569
- if (configured?.disabled === true) return failed("missing_key");
570
- const configuredOwnsJev = configured
571
- && providerMatchesRegistryTransport(JEV_PROVIDER_ID, configured);
572
- const apiKey = (
573
- configuredOwnsJev ? resolveProviderApiKey(configured.apiKey)?.trim() : undefined
574
- ) || process.env.TYPESAFE_API_KEY?.trim()
575
- || process.env.JEV_API_KEY?.trim();
576
- if (!apiKey) return failed("missing_key");
736
+ const endpoint = jevDecisionEndpoint(options.config, options.decisionProvider ?? JEV_PROVIDER_ID);
737
+ if (!endpoint) return failed("missing_key");
577
738
 
578
739
  let requestBody: string;
579
740
  try {
741
+ if (endpoint.descriptiveCriteria) {
742
+ // Self-hosted choice questions accept 2..26 options; decide locally instead of spending a
743
+ // round-trip on a request the service will refuse.
744
+ const optionCount = candidateOptions(options.candidates).size;
745
+ if (optionCount < SELF_HOSTED_MIN_OPTIONS) return failed("no_choices");
746
+ if (optionCount > SELF_HOSTED_MAX_OPTIONS) return failed("invalid");
747
+ }
580
748
  const state = buildJevState(options.body, options.candidates);
581
749
  if (!hasJevDecisionState(state)) return failed("no_state");
582
750
  requestBody = JSON.stringify({
583
- model: JEV_MODEL,
751
+ model: endpoint.model,
584
752
  state,
585
- questions: buildJevRouteQuestion(options.candidates),
753
+ questions: buildJevRouteQuestion(options.candidates, { descriptiveCriteria: endpoint.descriptiveCriteria }),
586
754
  });
587
755
  if (new TextEncoder().encode(requestBody).byteLength > JEV_MAX_REQUEST_BYTES) return failed("invalid");
588
756
  } catch {
589
757
  return failed("invalid");
590
758
  }
591
759
 
592
- const timeoutSignal = AbortSignal.timeout(JEV_TIMEOUT_MS);
760
+ const timeoutMs = jevDecisionTimeoutMs(options.timeoutMs);
761
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
593
762
  const signal = options.signal
594
763
  ? AbortSignal.any([options.signal, timeoutSignal])
595
764
  : timeoutSignal;
@@ -597,12 +766,12 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
597
766
 
598
767
  try {
599
768
  const response = await post(
600
- JEV_PROVIDER_ID,
601
- canonicalJevProvider(options.config),
602
- JEV_API_URL,
769
+ endpoint.name,
770
+ endpoint.provider,
771
+ endpoint.url,
603
772
  {
604
773
  headers: {
605
- Authorization: `Bearer ${apiKey}`,
774
+ ...(endpoint.apiKey ? { Authorization: `Bearer ${endpoint.apiKey}` } : {}),
606
775
  "Content-Type": "application/json",
607
776
  },
608
777
  body: requestBody,
@@ -612,7 +781,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
612
781
  );
613
782
  if (options.signal?.aborted) throw options.signal.reason;
614
783
 
615
- const redirectError = await providerRedirectError(response, JEV_API_URL);
784
+ const redirectError = await providerRedirectError(response, endpoint.url);
616
785
  if (redirectError) return failed("redirect");
617
786
  if (!response.ok) {
618
787
  try { void response.body?.cancel().catch(() => undefined); } catch { /* best effort */ }
@@ -642,6 +811,7 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
642
811
  }
643
812
  if (options.signal?.aborted) throw options.signal.reason;
644
813
  return {
814
+ backend,
645
815
  ...parsed,
646
816
  gate: "apply",
647
817
  latencyMs: Math.max(0, now() - startedAt),
@@ -655,3 +825,49 @@ export async function resolveJevDecision(options: ResolveJevDecisionOptions): Pr
655
825
  return failed("network");
656
826
  }
657
827
  }
828
+
829
+ export type JevDecisionProbeResult =
830
+ | { ok: true; latencyMs: number; message: string }
831
+ | { ok: false; latencyMs: number; error: string };
832
+
833
+ /**
834
+ * Management connection test for one `jev-decision` provider row. It sends a fixed probe task and
835
+ * no user prompt, and reports only a sanitized gate. Canonical TypeSafe wording is kept for `jev`.
836
+ */
837
+ export async function probeJevDecisionProvider(
838
+ config: OcxConfig,
839
+ name: string,
840
+ options: Pick<ResolveJevDecisionOptions, "signal" | "post"> = {},
841
+ ): Promise<JevDecisionProbeResult> {
842
+ const canonical = name === JEV_PROVIDER_ID;
843
+ const label = canonical ? "TypeSafe JEV" : "JEV decision service";
844
+ const probe = { targetKey: "jev/probe", effort: null } as const;
845
+ const decision = await resolveJevDecision({
846
+ body: {
847
+ input: canonical
848
+ ? "Verify the configured TypeSafe JEV decision service."
849
+ : "Verify the configured JEV decision service.",
850
+ },
851
+ candidates: [{
852
+ key: probe.targetKey,
853
+ provider: "jev",
854
+ model: "jev-latest",
855
+ // Self-hosted System One choice questions need at least two options.
856
+ reasoningEfforts: canonical ? [] : ["low", "high"],
857
+ }],
858
+ fallback: probe,
859
+ config,
860
+ decisionProvider: name,
861
+ ...options,
862
+ });
863
+ if (decision.gate === "apply") {
864
+ return { ok: true, latencyMs: decision.latencyMs, message: `Connected. ${label} answered a decision probe.` };
865
+ }
866
+ return {
867
+ ok: false,
868
+ latencyMs: decision.latencyMs,
869
+ error: decision.gate === "missing_key"
870
+ ? (canonical ? "TypeSafe JEV API key is not configured" : "JEV decision service is disabled or not configured")
871
+ : `${canonical ? "TypeSafe JEV decision probe" : "JEV decision service probe"} failed (${decision.gate})`,
872
+ };
873
+ }
@@ -1,10 +1,22 @@
1
- import { isCodexReasoningEffort } from "../reasoning-effort";
1
+ import { isCodexReasoningEffort, isDeclaredReasoningEffort } from "../reasoning-effort";
2
2
  import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
3
3
  import type { OcxComboConfig, OcxComboCooldownWaitPolicy, OcxComboDefaultEffort, OcxComboDefaultEffortMode, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
4
- import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
4
+ import { COMBO_NAMESPACE, isValidComboId, resolveComboId, targetKey } from "./identifiers";
5
+ import {
6
+ CANONICAL_JEV_DECISION_PROVIDER,
7
+ JEV_DECISION_TIMEOUT_MAX_MS,
8
+ JEV_DECISION_TIMEOUT_MIN_MS,
9
+ isSystemOneEndpoint,
10
+ } from "./jev-decision-contract";
5
11
 
6
12
  export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
7
13
  export const JEV_MAX_CANDIDATE_FIELD_CHARS = 512;
14
+ export {
15
+ JEV_DECISION_TIMEOUT_DEFAULT_MS,
16
+ JEV_DECISION_TIMEOUT_MAX_MS,
17
+ JEV_DECISION_TIMEOUT_MIN_MS,
18
+ isSystemOneEndpoint,
19
+ } from "./jev-decision-contract";
8
20
  export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers";
9
21
 
10
22
  /**
@@ -52,6 +64,12 @@ export interface NormalizedComboConfig {
52
64
  nativeAlias: boolean;
53
65
  /** Display-only label for the catalog row, or null when unset. */
54
66
  displayName: string | null;
67
+ /** JEV decision service provider id; absent means the canonical `jev` service. */
68
+ decisionProvider?: string;
69
+ /** Ordinary inference route used for JEV decisions. */
70
+ decisionModel?: string;
71
+ /** JEV decision deadline override; absent keeps the default four-second deadline. */
72
+ decisionTimeoutMs?: number;
55
73
  targets: NormalizedComboTarget[];
56
74
  }
57
75
 
@@ -102,8 +120,15 @@ export function comboAliasIssues(
102
120
 
103
121
  export interface ComboValidationOptions {
104
122
  requireEnabledTarget?: boolean;
123
+ /**
124
+ * Save-time only: also reject a `decisionProvider` row the runtime would skip (disabled, or no
125
+ * model). Config-file load stays lenient so disabling a referenced row never breaks startup.
126
+ */
127
+ requireUsableDecisionService?: boolean;
105
128
  /** Full combos map for alias uniqueness checks; omitted during early config load. */
106
129
  combos?: Record<string, OcxComboConfig>;
130
+ /** Ingress selector grammar, injected so combo validation never imports server routing. */
131
+ normalizeDecisionModel?: (model: string) => string;
107
132
  /** Combo being renamed — its stored alias is excluded from uniqueness checks. */
108
133
  excludeComboId?: string;
109
134
  }
@@ -256,6 +281,72 @@ export function comboConfigIssues(
256
281
  if (nativeAlias && (typeof body.displayName !== "string" || body.displayName.trim().length === 0)) {
257
282
  issues.push({ path: ["displayName"], message: "displayName is required for native aliases" });
258
283
  }
284
+ if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
285
+ const decisionProvider = typeof body.decisionProvider === "string" ? body.decisionProvider.trim() : "";
286
+ if (!decisionProvider) {
287
+ issues.push({ path: ["decisionProvider"], message: "decisionProvider must be a non-empty provider name" });
288
+ } else if (body.strategy !== "jev") {
289
+ issues.push({ path: ["decisionProvider"], message: 'decisionProvider is only valid with strategy "jev"' });
290
+ } else if (decisionProvider === CANONICAL_JEV_DECISION_PROVIDER) {
291
+ // Explicit "jev" means exactly what omission means: the canonical TypeSafe service.
292
+ } else if (!Object.hasOwn(providers, decisionProvider)) {
293
+ issues.push({
294
+ path: ["decisionProvider"],
295
+ message: `decisionProvider "${decisionProvider}" is not configured`,
296
+ });
297
+ } else if (providers[decisionProvider]?.adapter !== "jev-decision") {
298
+ issues.push({
299
+ path: ["decisionProvider"],
300
+ message: `decisionProvider "${decisionProvider}" is not a decision service (adapter must be "jev-decision")`,
301
+ });
302
+ } else if (!isSystemOneEndpoint(String(providers[decisionProvider]?.baseUrl ?? ""))) {
303
+ issues.push({
304
+ path: ["decisionProvider"],
305
+ message: `decisionProvider "${decisionProvider}" baseUrl must be the full decision endpoint ending in /systemone`,
306
+ });
307
+ } else if (options.requireUsableDecisionService && providers[decisionProvider]?.disabled === true) {
308
+ issues.push({
309
+ path: ["decisionProvider"],
310
+ message: `decisionProvider "${decisionProvider}" is disabled`,
311
+ });
312
+ } else if (options.requireUsableDecisionService
313
+ && !providers[decisionProvider]?.defaultModel?.trim()
314
+ && !providers[decisionProvider]?.models?.[0]?.trim()) {
315
+ issues.push({
316
+ path: ["decisionProvider"],
317
+ message: `decisionProvider "${decisionProvider}" has no model (set defaultModel or models)`,
318
+ });
319
+ }
320
+ }
321
+ if (body.decisionModel !== undefined && body.decisionModel !== null) {
322
+ const model = typeof body.decisionModel === "string" ? body.decisionModel.trim() : "";
323
+ if (!model || model.length > JEV_MAX_CANDIDATE_FIELD_CHARS) {
324
+ issues.push({ path: ["decisionModel"], message: `decisionModel must be a non-empty string of at most ${JEV_MAX_CANDIDATE_FIELD_CHARS} characters` });
325
+ } else {
326
+ if (body.strategy !== "jev") {
327
+ issues.push({ path: ["decisionModel"], message: 'decisionModel is only valid with strategy "jev"' });
328
+ }
329
+ if (body.decisionProvider !== undefined && body.decisionProvider !== null) {
330
+ issues.push({ path: ["decisionModel"], message: "decisionModel cannot coexist with decisionProvider" });
331
+ }
332
+ const comboId = resolveComboId({ combos: options.combos }, options.normalizeDecisionModel?.(model) ?? model);
333
+ if (comboId === id || (comboId && options.combos?.[comboId]?.strategy === "jev")) {
334
+ issues.push({ path: ["decisionModel"], message: `decisionModel must not reference ${comboId === id ? "itself" : "a JEV combo"} (combo "${comboId}")` });
335
+ }
336
+ }
337
+ }
338
+ if (body.decisionTimeoutMs !== undefined && body.decisionTimeoutMs !== null) {
339
+ if (typeof body.decisionTimeoutMs !== "number" || !Number.isInteger(body.decisionTimeoutMs)
340
+ || body.decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS
341
+ || body.decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS) {
342
+ issues.push({
343
+ path: ["decisionTimeoutMs"],
344
+ message: `decisionTimeoutMs must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}`,
345
+ });
346
+ } else if (body.strategy !== "jev") {
347
+ issues.push({ path: ["decisionTimeoutMs"], message: 'decisionTimeoutMs is only valid with strategy "jev"' });
348
+ }
349
+ }
259
350
 
260
351
  if (!Array.isArray(body.targets) || body.targets.length === 0) {
261
352
  issues.push({ path: ["targets"], message: "targets must be a non-empty array" });
@@ -379,6 +470,8 @@ export function comboConfigError(
379
470
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
380
471
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
381
472
  const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
473
+ const decisionProvider = typeof raw.decisionProvider === "string" ? raw.decisionProvider.trim() : "";
474
+ const decisionModel = typeof raw.decisionModel === "string" ? raw.decisionModel.trim() : "";
382
475
  const defaultEffort = typeof raw.defaultEffort === "string" && isCodexReasoningEffort(raw.defaultEffort)
383
476
  ? raw.defaultEffort
384
477
  : null;
@@ -395,6 +488,10 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
395
488
  alias: alias || null,
396
489
  nativeAlias: raw.nativeAlias === true,
397
490
  displayName: displayName || null,
491
+ // Explicit "jev" is the default and stays sparse.
492
+ ...(decisionProvider && decisionProvider !== CANONICAL_JEV_DECISION_PROVIDER ? { decisionProvider } : {}),
493
+ ...(decisionModel ? { decisionModel } : {}),
494
+ ...(typeof raw.decisionTimeoutMs === "number" ? { decisionTimeoutMs: raw.decisionTimeoutMs } : {}),
398
495
  targets: raw.targets.map(target => ({
399
496
  provider: target.provider.trim(),
400
497
  model: target.model.trim(),
@@ -410,6 +507,32 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
410
507
  };
411
508
  }
412
509
 
510
+ /**
511
+ * Load-time stand-in for the ingress synthetic-row grammar: strip a `--fast` suffix, and a
512
+ * `--<effort>` suffix when Cursor effort rows are on. It reads only the string, so schema
513
+ * validation needs no server module, model inventory, or Cursor install detection. The
514
+ * management save path still resolves the exact grammar against the live inventory.
515
+ */
516
+ export function lexicalDecisionModelBase(model: string, cursorEffortRows: boolean): string {
517
+ if (model.endsWith("--fast")) return model.slice(0, -"--fast".length);
518
+ if (!cursorEffortRows) return model;
519
+ const separator = model.lastIndexOf("--");
520
+ if (separator <= 0) return model;
521
+ const effort = model.slice(separator + 2);
522
+ return effort !== "none" && isDeclaredReasoningEffort(effort) ? model.slice(0, separator) : model;
523
+ }
524
+
525
+ /**
526
+ * Whether removing `provider` would leave this stored combo invalid: a target uses it, or the
527
+ * combo names it as its JEV decision service. The canonical `jev` id stays valid without a row.
528
+ */
529
+ export function comboDependsOnProvider(combo: OcxComboConfig, provider: string): boolean {
530
+ if (combo.targets.some(target => target.provider === provider)) return true;
531
+ if (typeof combo.decisionModel === "string" && combo.decisionModel.trim().startsWith(`${provider}/`)) return true;
532
+ const decisionProvider = typeof combo.decisionProvider === "string" ? combo.decisionProvider.trim() : "";
533
+ return decisionProvider === provider && provider !== CANONICAL_JEV_DECISION_PROVIDER;
534
+ }
535
+
413
536
  export function comboDefaultEffort(
414
537
  config: { combos?: Record<string, OcxComboConfig> },
415
538
  id: string,
@@ -64,6 +64,7 @@ import {
64
64
  remoteGuiConfigSchema,
65
65
  runtimeRoleSchema,
66
66
  spendSchema,
67
+ chatgptDesktopSchema,
67
68
  compactionRoutingSchema,
68
69
  skillsConfigSchema,
69
70
  memoryModelsSchema,
@@ -102,6 +103,9 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
102
103
  const rawEffort = rawClaudeSubagentEffort(rawParsed);
103
104
  const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
104
105
  const warnings = configPlaceholderWarnings(normalized);
106
+ if (normalized.chatgptDesktop?.appServerShim === true && process.platform !== "darwin") {
107
+ warnings.push("chatgptDesktop.appServerShim is experimental and macOS only; ignored on this platform");
108
+ }
105
109
  warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
106
110
  warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
107
111
  warnings.push(...degradedListenerWarnings(rawParsed, normalized));
@@ -463,6 +467,15 @@ function showCodexCreditsError(value: unknown): string | null {
463
467
  return "schema_invalid: showCodexCredits: must be a boolean or omitted";
464
468
  }
465
469
 
470
+ function creditCodexAccountIdsError(value: unknown): string | null {
471
+ const raw = rawConfigRecord(value);
472
+ if (!raw || !Object.hasOwn(raw, "creditCodexAccountIds")) return null;
473
+ const ids = raw.creditCodexAccountIds;
474
+ if (ids === undefined) return null;
475
+ if (Array.isArray(ids) && ids.every(id => typeof id === "string" && /^[a-zA-Z0-9._-]{1,64}$/.test(id))) return null;
476
+ return "schema_invalid: creditCodexAccountIds: must be an array of account ids or omitted";
477
+ }
478
+
466
479
  function oauthOpenBrowserError(value: unknown): string | null {
467
480
  const raw = rawConfigRecord(value);
468
481
  if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
@@ -619,6 +632,10 @@ function skillsConfigError(value: unknown): string | null {
619
632
  }
620
633
 
621
634
  export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
635
+ const chatgptDesktop = rawConfigRecord(value)?.chatgptDesktop;
636
+ if (chatgptDesktop !== undefined && !chatgptDesktopSchema.safeParse(chatgptDesktop).success) {
637
+ return { ok: false, error: "schema_invalid: chatgptDesktop: requires an optional boolean appServerShim and no other fields" };
638
+ }
622
639
  const compactionRouting = rawConfigRecord(value)?.compactionRouting;
623
640
  if (compactionRouting !== undefined && !compactionRoutingSchema.safeParse(compactionRouting).success) {
624
641
  return { ok: false, error: "schema_invalid: compactionRouting: requires a nonblank model, an optional valid reasoningEffort, and optional non-repeating triggers drawn from \"manual\" and \"auto\"" };
@@ -654,6 +671,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
654
671
  ?? dropCodexSafetyBufferingError(value)
655
672
  ?? oauthOpenBrowserError(value)
656
673
  ?? showCodexCreditsError(value)
674
+ ?? creditCodexAccountIdsError(value)
657
675
  ?? runtimeRoleError(value)
658
676
  ?? remoteGuiConfigError(value)
659
677
  ?? clientConnectionConfigError(value)