@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
@@ -2,10 +2,14 @@ import { execFileSync } from "node:child_process";
2
2
  import { isIP } from "node:net";
3
3
 
4
4
  export type MacOSProxyReader = () => string | null;
5
+ export type UnrepresentableCounts = { cidr: number; hostname: number; wildcard: number; other: number };
5
6
  export type MacOSSystemProxyResult =
6
7
  | { kind: "proxy"; httpUrl?: string; httpsUrl?: string; exceptions: string[]; droppedLinkLocal: boolean }
7
- | { kind: "disabled" | "unreadable" | "unsafe-exceptions" };
8
+ | { kind: "unsafe-exceptions"; unrepresentable?: UnrepresentableCounts; setting?: string }
9
+ | { kind: "socks-only" }
10
+ | { kind: "disabled" | "unreadable" };
8
11
 
12
+ /** Reads the global proxy dictionary from /usr/sbin/scutil with tight time and size limits. */
9
13
  function readScutilProxy(): string {
10
14
  return execFileSync("/usr/sbin/scutil", ["--proxy"], {
11
15
  encoding: "utf8",
@@ -15,6 +19,7 @@ function readScutilProxy(): string {
15
19
  });
16
20
  }
17
21
 
22
+ /** Normalizes an enabled host/port pair into an origin URL, or undefined when unusable. */
18
23
  function proxyUrl(host: string | undefined, port: string | undefined): string | undefined {
19
24
  if (!host || !port || !/^\d+$/.test(port) || +port < 1 || +port > 65535) return undefined;
20
25
  const bareHost = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
@@ -26,11 +31,17 @@ function proxyUrl(host: string | undefined, port: string | undefined): string |
26
31
  }
27
32
  }
28
33
 
29
- // Bun matches a leading-dot entry at DNS-label boundaries and also bypasses the
30
- // bare apex. Translating "*.local" to ".local" therefore widens only to "local";
31
- // other glob shapes are refused. Bun cannot represent the default link-local
32
- // CIDRs, so they are dropped with a diagnostic instead of blocking discovery.
33
- // null means one of those exact ranges was dropped; undefined refuses discovery.
34
+ /**
35
+ * Bun matches a leading-dot entry at DNS-label boundaries and also bypasses the
36
+ * bare apex. Translating "*.local" to ".local" therefore widens only to "local";
37
+ * other glob shapes are refused. Bun cannot represent the default link-local
38
+ * CIDRs, so they are dropped with a diagnostic instead of blocking discovery.
39
+ * Returns the translated entry, null when one of those exact ranges was dropped,
40
+ * or undefined when the shape cannot be represented and refuses discovery.
41
+ * Non-canonical IPv4 literals (e.g. leading-zero octets) return undefined; the
42
+ * refusal counter then reports them in the "other" shape bucket (see
43
+ * unrepresentableCategory) so shape counts stay interpretable.
44
+ */
34
45
  function translateException(value: string): string | null | undefined {
35
46
  if (value === "*") return value;
36
47
  if (value === "169.254/16" || value === "169.254.0.0/16") return null;
@@ -52,6 +63,32 @@ function translateException(value: string): string | null | undefined {
52
63
  return isIP(bare) === 6 ? new URL(`http://[${bare}]`).hostname : undefined;
53
64
  }
54
65
 
66
+ /**
67
+ * Classifies an unrepresentable entry for refusal diagnostics. Refusals must not echo
68
+ * the offending entries (a system bypass list can name internal hosts), so discovery
69
+ * reports only how many entries fall into each shape: CIDR ranges, syntactically
70
+ * valid bare hostnames Bun would widen to subdomains (validated per DNS label, so
71
+ * malformed structures like "foo..bar" do not count), malformed wildcard shapes,
72
+ * and anything else. Non-canonical IPv4 literals (digit-dotted strings that fail
73
+ * canonicalization) are IP-shaped rather than hostnames and count as "other".
74
+ */
75
+ function unrepresentableCategory(value: string): "cidr" | "hostname" | "wildcard" | "other" {
76
+ if (value.includes("/")) return "cidr";
77
+ if (value.includes("*")) return "wildcard";
78
+ if (isIP(value) || (value.startsWith("[") && value.endsWith("]"))) return "other";
79
+ if (/^\d+(\.\d+)+$/.test(value)) return "other";
80
+ const validLabels = value.split(".").every(label => label.length > 0 && label.length <= 63
81
+ && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
82
+ return validLabels ? "hostname" : "other";
83
+ }
84
+
85
+ /** Counts unrepresentable entries by shape for the refusal diagnostic. */
86
+ function countUnrepresentable(values: string[]): UnrepresentableCounts {
87
+ const counts: UnrepresentableCounts = { cidr: 0, hostname: 0, wildcard: 0, other: 0 };
88
+ for (const value of values) counts[unrepresentableCategory(value)]++;
89
+ return counts;
90
+ }
91
+
55
92
  /** Read only the global dictionary; scoped service dictionaries do not apply globally. */
56
93
  export function readMacOSSystemProxy(reader: MacOSProxyReader = readScutilProxy): MacOSSystemProxyResult {
57
94
  try {
@@ -90,24 +127,44 @@ export function readMacOSSystemProxy(reader: MacOSProxyReader = readScutilProxy)
90
127
  }
91
128
  }
92
129
  if (depth !== 0) return { kind: "unreadable" };
93
- if (values.get("ExcludeSimpleHostnames") === "1" || values.get("ProxyAutoConfigEnable") === "1"
94
- || values.get("ProxyAutoDiscoveryEnable") === "1") return { kind: "unsafe-exceptions" };
95
130
  for (const key of ["HTTPEnable", "HTTPSEnable", "ExcludeSimpleHostnames", "ProxyAutoConfigEnable", "ProxyAutoDiscoveryEnable"]) {
96
131
  const value = values.get(key);
97
132
  if (value !== undefined && value !== "0" && value !== "1") return { kind: "unreadable" };
98
133
  }
99
- const translated = exceptions.map(translateException);
100
- if (translated.some(value => value === undefined)) return { kind: "unsafe-exceptions" };
101
134
  const httpEnabled = values.get("HTTPEnable") === "1";
102
135
  const httpsEnabled = values.get("HTTPSEnable") === "1";
103
136
  const httpUrl = httpEnabled ? proxyUrl(values.get("HTTPProxy"), values.get("HTTPPort")) : undefined;
104
137
  const httpsUrl = httpsEnabled ? proxyUrl(values.get("HTTPSProxy"), values.get("HTTPSPort")) : undefined;
105
138
  if ((httpEnabled && !httpUrl) || (httpsEnabled && !httpsUrl)) return { kind: "unreadable" };
106
- return httpUrl || httpsUrl
107
- ? { kind: "proxy", httpUrl, httpsUrl,
108
- exceptions: translated.filter((value): value is string => typeof value === "string"),
109
- droppedLinkLocal: translated.includes(null) }
110
- : { kind: "disabled" };
139
+ // Transport precedence: with no HTTP(S) proxy configured, no bypass list or toggle
140
+ // can change the outcome — SOCKS-only is the primary blocker and is reported ahead
141
+ // of exception shapes, which would otherwise mask it (review round 1, P2).
142
+ if (!(httpUrl || httpsUrl)) {
143
+ // PAC and WPAD can route traffic without a static HTTP(S) proxy. Saying "disabled",
144
+ // or "SOCKS-only, using direct egress", would misdescribe that setup, so the
145
+ // automatic-configuration toggle is reported first.
146
+ const autoSetting = values.get("ProxyAutoConfigEnable") === "1" ? "ProxyAutoConfigEnable"
147
+ : values.get("ProxyAutoDiscoveryEnable") === "1" ? "ProxyAutoDiscoveryEnable"
148
+ : undefined;
149
+ if (autoSetting) return { kind: "unsafe-exceptions", setting: autoSetting };
150
+ return values.get("SOCKSEnable") === "1" ? { kind: "socks-only" } : { kind: "disabled" };
151
+ }
152
+ // With a usable HTTP(S) transport, toggles and untranslatable entries refuse
153
+ // discovery together: the user should fix both in one pass, not one per retry.
154
+ const flaggedSetting = values.get("ExcludeSimpleHostnames") === "1" ? "ExcludeSimpleHostnames"
155
+ : values.get("ProxyAutoConfigEnable") === "1" ? "ProxyAutoConfigEnable"
156
+ : values.get("ProxyAutoDiscoveryEnable") === "1" ? "ProxyAutoDiscoveryEnable"
157
+ : undefined;
158
+ const translated = exceptions.map(translateException);
159
+ const unrepresentable = translated.some(value => value === undefined)
160
+ ? countUnrepresentable(exceptions.filter((_, index) => translated[index] === undefined))
161
+ : undefined;
162
+ if (flaggedSetting || unrepresentable) {
163
+ return { kind: "unsafe-exceptions", setting: flaggedSetting, unrepresentable };
164
+ }
165
+ return { kind: "proxy", httpUrl, httpsUrl,
166
+ exceptions: translated.filter((value): value is string => typeof value === "string"),
167
+ droppedLinkLocal: translated.includes(null) };
111
168
  } catch {
112
169
  return { kind: "unreadable" };
113
170
  }
@@ -24,12 +24,13 @@
24
24
  */
25
25
 
26
26
  import { createHash } from "node:crypto";
27
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
27
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join, resolve } from "node:path";
30
- import { atomicWriteFile } from "./atomic-write";
30
+ import { atomicWriteFileNoFollow } from "./atomic-write";
31
31
  import { getConfigDir } from "./paths";
32
32
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
33
+ import { hardenSecretDir } from "../lib/windows-secret-acl";
33
34
 
34
35
  const REGISTRY_DIR_NAME = "ocx-homes";
35
36
  /**
@@ -83,8 +84,16 @@ export function registerOwnerRegistryHome(home: string): void {
83
84
  if (parent === ancestor) break;
84
85
  ancestor = parent;
85
86
  }
86
- mkdirSync(dir, { recursive: true });
87
- atomicWriteFile(
87
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
88
+ // This locator contains no secret, but its deterministic entry names must not
89
+ // give another local user a place to plant a destination symlink. Refuse a
90
+ // directory symlink and fail closed if an existing directory cannot be made
91
+ // owner-only (for example because another user created it first).
92
+ const registryStat = lstatSync(dir);
93
+ if (!registryStat.isDirectory() || registryStat.isSymbolicLink()) return;
94
+ chmodSync(dir, 0o700);
95
+ hardenSecretDir(dir, { required: true });
96
+ atomicWriteFileNoFollow(
88
97
  registryEntryPath(dir, home),
89
98
  JSON.stringify({ home: resolve(home), v: 1 }) + "\n",
90
99
  );
@@ -328,14 +328,23 @@ export function readProcessCommandLine(pid: number): string | undefined {
328
328
  } catch {
329
329
  /* WMIC missing or failed — fall through */
330
330
  }
331
- const output = processCommandLineExec(resolveTrustedWindowsPowerShellExe(), [
332
- "-NoProfile",
333
- "-NoLogo",
334
- "-NonInteractive",
335
- "-Command",
336
- `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
337
- ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
338
- return output.trim() || undefined;
331
+ // One bounded retry: a transient CIM timeout answers "unreadable" for an
332
+ // unchanged process and turns a guarded stop into a false approval-changed.
333
+ for (let attempt = 0; attempt < 2; attempt += 1) {
334
+ try {
335
+ const output = processCommandLineExec(resolveTrustedWindowsPowerShellExe(), [
336
+ "-NoProfile",
337
+ "-NoLogo",
338
+ "-NonInteractive",
339
+ "-Command",
340
+ `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
341
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
342
+ return output.trim() || undefined;
343
+ } catch {
344
+ /* timed out or CIM unavailable — try once more, then unreadable */
345
+ }
346
+ }
347
+ return undefined;
339
348
  }
340
349
  for (const ps of ["/bin/ps", "/usr/bin/ps"]) {
341
350
  try {
@@ -5,7 +5,7 @@ import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-mod
5
5
  import { MULTI_AGENT_SURFACE_ADVISORY_VERSION } from "./multi-agent-surface";
6
6
  import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES } from "../lib/app-owned-memory";
7
7
  import { describeProxyForLog, readWindowsSystemProxy, type WindowsProxyRegistryReader } from "../lib/windows-system-proxy";
8
- import { readMacOSSystemProxy, type MacOSProxyReader } from "./macos-system-proxy";
8
+ import { readMacOSSystemProxy, type MacOSProxyReader, type MacOSSystemProxyResult } from "./macos-system-proxy";
9
9
  import { OPENAI_PROVIDER_TIER_VERSION, type OcxConfig } from "../types";
10
10
  import type { OcxRuntimeRole } from "../types/config";
11
11
 
@@ -187,6 +187,34 @@ export function applyProxyEnv(config: OcxConfig, announce = false): void {
187
187
  if (outbound) console.log(` outbound proxy: ${redactUrlForLog(outbound)}`);
188
188
  }
189
189
 
190
+ /**
191
+ * Builds the refusal reason for an unsafe macOS discovery outcome. Toggle refusals
192
+ * name the toggle only — they are not an exception-translation problem, so the
193
+ * "exceptions" framing is reserved for entries. Entry refusals report shape counts
194
+ * and setting names, never the entries themselves: naming them would leak
195
+ * bypass-list hostnames (internal servers, banked domains) into shared logs.
196
+ */
197
+ function describeDiscoveryRefusal(
198
+ found: Extract<MacOSSystemProxyResult, { kind: "unsafe-exceptions" }>,
199
+ ): string {
200
+ const counts = found.unrepresentable;
201
+ if (counts) {
202
+ const parts = [
203
+ counts.cidr > 0 && `${counts.cidr} CIDR`,
204
+ counts.hostname > 0 && `${counts.hostname} bare-hostname`,
205
+ counts.wildcard > 0 && `${counts.wildcard} wildcard-shaped`,
206
+ counts.other > 0 && `${counts.other} other`,
207
+ ].filter(Boolean).join(", ");
208
+ const entries = counts.cidr + counts.hostname + counts.wildcard + counts.other;
209
+ const entriesPhrase = `${entries} ${entries === 1 ? "exception entry" : "exception entries"} cannot be safely translated (${parts})`;
210
+ return found.setting
211
+ ? `${found.setting} is enabled; ${entriesPhrase}; discovery refused`
212
+ : `macOS exceptions cannot be safely translated (${parts}); discovery refused`;
213
+ }
214
+ if (found.setting) return `${found.setting} is enabled; discovery refused`;
215
+ return "macOS exceptions cannot be safely translated; discovery refused";
216
+ }
217
+
190
218
  /** Test seam for `proxy: "auto"`: the registry reader and platform are injectable. */
191
219
  export function applyProxyEnvWith(
192
220
  config: OcxConfig,
@@ -224,10 +252,12 @@ export function applyProxyEnvWith(
224
252
  const found = readMacOSSystemProxy(auto.macOSReader);
225
253
  if (found.kind !== "proxy") {
226
254
  const reason = found.kind === "unsafe-exceptions"
227
- ? "macOS exceptions cannot be safely translated; discovery refused"
228
- : found.kind === "disabled"
229
- ? "macOS system proxy is disabled"
230
- : "macOS proxy settings could not be read";
255
+ ? describeDiscoveryRefusal(found)
256
+ : found.kind === "socks-only"
257
+ ? "macOS system proxy is SOCKS-only, which HTTP_PROXY cannot express; using direct egress"
258
+ : found.kind === "disabled"
259
+ ? "macOS system proxy is disabled"
260
+ : "macOS proxy settings could not be read";
231
261
  console.log(`[opencodex] proxy "auto": ${reason}; proxy environment unchanged`);
232
262
  return;
233
263
  }
@@ -1,3 +1,4 @@
1
+ import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
1
2
  import * as z from "zod/v4";
2
3
  import { compactionRecoverySchema } from "./compaction-recovery";
3
4
  import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
@@ -17,6 +18,7 @@ import {
17
18
  remoteGuiConfigSchema,
18
19
  runtimeRoleSchema,
19
20
  spendSchema,
21
+ chatgptDesktopSchema,
20
22
  skillsConfigSchema,
21
23
  configuredCodexPoolAccountIds,
22
24
  apiKeyEntrySchema,
@@ -53,10 +55,13 @@ import {
53
55
  } from "../../codex/account-namespace-match";
54
56
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../../codex/upstream-host-health";
55
57
  import { MIN_USAGE_LEDGER_MAX_BYTES } from "../../usage/retention-contract";
56
- import { COMBO_NAMESPACE, comboConfigIssues } from "../../combos/types";
58
+ // The schema boundary uses a string-only stand-in for the ingress grammar: importing the server
59
+ // parser here would cycle back through config loading and run Cursor detection during validation.
60
+ import { COMBO_NAMESPACE, comboConfigIssues, lexicalDecisionModelBase } from "../../combos/types";
57
61
  import { routingProfileIssues } from "../../routing/profile";
58
62
  import { POLICY_NAMESPACE } from "../../routing/profile-namespace";
59
63
  import { providerDestinationConfigError } from "../../lib/destination-policy";
64
+ import { providerTlsProfileConfigError } from "../../lib/provider-tls-profile";
60
65
  import { redactSecretString } from "../../lib/redact";
61
66
  import { openRouterRoutingConfigError } from "../../providers/openrouter-routing";
62
67
  import { vercelGatewayRoutingConfigError } from "../../providers/vercel-gateway-routing";
@@ -69,6 +74,7 @@ import { isInterceptBindingId, isInterceptBindingRoute } from "../../claude/inte
69
74
  import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
70
75
 
71
76
  export const configSchema = z.object({
77
+ chatgptDesktop: chatgptDesktopSchema.optional().catch(undefined),
72
78
  codexNativeSteering: z.boolean().optional().catch(false),
73
79
  codexNativeInjection: z.boolean().optional().catch(false),
74
80
  port: z.number().int().min(0).max(65535).default(10100),
@@ -186,6 +192,10 @@ export const configSchema = z.object({
186
192
  // Default-on policy (#5694): absence and malformed hand edits both mean "on", and only an
187
193
  // explicit `false` written by the settings PUT opts out.
188
194
  codexMainAccountHardLock: z.boolean().optional().catch(undefined),
195
+ codexMainAccountHardLockThresholds: z.object({
196
+ short: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
197
+ long: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
198
+ }).optional().catch(undefined),
189
199
  // Future versions remain opaque through passthrough-compatible whole-config saves.
190
200
  // Only version 1 grants deletion authority in the rebase path.
191
201
  configRebaseProvenance: z.unknown().optional(),
@@ -237,6 +247,12 @@ export const configSchema = z.object({
237
247
  z.string(),
238
248
  z.array(z.string().trim().min(1)).min(1),
239
249
  ).optional().catch(undefined),
250
+ // Advisory input to role auto-assign only; a malformed block falls back to price ranking.
251
+ codexRoleTiers: z.object({
252
+ fast: z.array(z.string().trim().min(1)).optional(),
253
+ standard: z.array(z.string().trim().min(1)).optional(),
254
+ frontier: z.array(z.string().trim().min(1)).optional(),
255
+ }).strict().optional().catch(undefined),
240
256
  codexShimAutoRestore: z.boolean().optional(),
241
257
  codexDesktopAuthless: z.boolean().optional().catch(undefined),
242
258
  codexClientCompaction: z.boolean().optional().catch(undefined),
@@ -245,6 +261,10 @@ export const configSchema = z.object({
245
261
  // would refuse to load — the provider id routing depends on is never derived from it.
246
262
  codexProviderDisplayName: z.string().trim().min(1).max(128).optional().catch(undefined),
247
263
  pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
264
+ // A malformed allow-list degrades to "no account spends credits" rather than failing the parse,
265
+ // so a hand-edited typo cannot trip the backup-and-defaults repair path; the write path rejects
266
+ // it (creditCodexAccountIdsError in diagnostics).
267
+ creditCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional().catch(undefined),
248
268
  // A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
249
269
  // typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
250
270
  // Silently ignoring it would be its own trap, so the write path rejects it and loadConfig warns.
@@ -470,6 +490,14 @@ export const configSchema = z.object({
470
490
  });
471
491
  }
472
492
  }
493
+ const tlsProfileError = providerTlsProfileConfigError(name, provider);
494
+ if (tlsProfileError) {
495
+ ctx.addIssue({
496
+ code: "custom",
497
+ path: ["providers", redactSecretString(name), "tlsProfile"],
498
+ message: tlsProfileError,
499
+ });
500
+ }
473
501
  const headersError = providerHeadersConfigError((provider as { headers?: unknown }).headers);
474
502
  if (headersError) {
475
503
  ctx.addIssue({
@@ -720,6 +748,7 @@ export const configSchema = z.object({
720
748
  for (const issue of comboConfigIssues(id, raw, config.providers, {
721
749
  combos: combos as Record<string, import("../../types").OcxComboConfig>,
722
750
  excludeComboId: id,
751
+ normalizeDecisionModel: model => lexicalDecisionModelBase(model, config.cursorEffortRows === true),
723
752
  })) {
724
753
  ctx.addIssue({
725
754
  code: "custom",
@@ -43,6 +43,9 @@ import {
43
43
  import { getConfigDir } from "../paths";
44
44
  import { COMPACTION_TRIGGERS } from "./compaction-triggers";
45
45
 
46
+ /** Experimental macOS ChatGPT app-server shim; only a strict optional boolean is accepted. */
47
+ export const chatgptDesktopSchema = z.object({ appServerShim: z.boolean().optional() }).strict();
48
+
46
49
  /** One definition of "usable secret", shared by the schema and the warnings. */
47
50
  export function isUsableApiKeySecret(value: unknown): value is string {
48
51
  return typeof value === "string" && value.length > 0 && value === value.trim();
@@ -294,6 +297,7 @@ export const providerConfigSchema = z.object({
294
297
  autoReviewModelOverrides: autoReviewModelOverridesSchema.optional(),
295
298
  adapter: z.string().min(1),
296
299
  baseUrl: z.string().min(1),
300
+ tlsProfile: z.literal("antigravity-browser").optional(),
297
301
  alias: z.string().optional(),
298
302
  modelAliases: z.record(z.string(), z.string()).optional(),
299
303
  modelDisplayNames: modelDisplayNamesSchema.optional(),
@@ -33,7 +33,14 @@ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
33
33
  import { atomicWriteFile } from "./atomic-write";
34
34
  import { getConfigDir } from "./paths";
35
35
  import { ConfigMutationLockError, withConfigMutationLockSync } from "./mutation-lock";
36
- import { SERVICE_MANAGED_ENV } from "../service/state";
36
+ import {
37
+ inspectServiceManagerInstallation,
38
+ queryWinswBinaryPathName,
39
+ scBinaryPathNamesExecutable,
40
+ type ServiceManagerClaim,
41
+ type ServiceManagerInstallation,
42
+ } from "../service-manager-probe";
43
+ import { compareServicePathToInstall, currentServiceHomes, SERVICE_MANAGED_ENV } from "../service/state";
37
44
  import { WINDOWS_WRAPPER_PROTOCOL_ENV } from "../service/windows-wrapper-exit";
38
45
 
39
46
  export function servingRuntimesPath(dir: string = getConfigDir()): string {
@@ -253,11 +260,38 @@ export function selectNewerServingRuntime(
253
260
  return best === null ? null : { ...best.record, version: best.version };
254
261
  }
255
262
 
263
+ /**
264
+ * Whether the census names a command different from self that records a newer
265
+ * version and still exists on disk. Cheap and spawn-free: use it to gate the
266
+ * expensive service-manager provenance probe instead of probing on every start.
267
+ */
268
+ export function hasNewerRecordedServingRuntime(
269
+ selfVersion: string,
270
+ selfCommand: readonly string[],
271
+ deps: NewerServingRuntimeDeps = {},
272
+ ): boolean {
273
+ const self = parseStrictSemver(selfVersion);
274
+ if (self === null) return false;
275
+ const exists = deps.exists ?? existsSync;
276
+ const selfKey = servingRuntimeCommandKey(selfCommand);
277
+ return readServingRuntimes(deps.dir ?? getConfigDir())
278
+ .filter(record => servingRuntimeCommandKey(record.command) !== selfKey)
279
+ .filter(record => {
280
+ const recorded = parseStrictSemver(record.version);
281
+ return recorded !== null && compareStrictSemver(recorded, self) > 0;
282
+ })
283
+ .some(record => record.command.every(part => exists(part) && trustedRecordedPath(part)));
284
+ }
285
+
256
286
  export interface DeferToNewerRuntimeDeps extends NewerServingRuntimeDeps {
257
287
  readonly runInherited?: (command: readonly string[], args: readonly string[]) => Promise<DelegatedExit>;
258
288
  readonly log?: (line: string) => void;
259
289
  /** Environment the service manager gave this child; decides its stay-out exit code. */
260
290
  readonly env?: NodeJS.ProcessEnv;
291
+ /** Test seam for the manager-definition provenance check at the pre-probe gate. */
292
+ readonly installedServiceOwnsCurrentHome?: () => boolean;
293
+ /** Test seam for the recorded-candidate gate that keeps the manager probe off empty censuses. */
294
+ readonly hasNewerRecordedRuntime?: () => boolean;
261
295
  }
262
296
 
263
297
  interface DelegatedExit { readonly exitCode: number; readonly ready: boolean }
@@ -397,9 +431,69 @@ export async function deferServiceChildToNewerRuntime(options: {
397
431
  readonly deps?: DeferToNewerRuntimeDeps;
398
432
  }): Promise<number | null> {
399
433
  if (options.sibling || !isManagedServiceEnvironment(options.env) || options.env[DELEGATED_ONCE_ENV] === "1") return null;
434
+ // Cheap gate first: with no recorded newer candidate there is nothing to delegate to, so
435
+ // the manager-definition probe (a potential full Task Scheduler listing) must not run.
436
+ const hasCandidate = options.deps?.hasNewerRecordedRuntime
437
+ ?? (() => hasNewerRecordedServingRuntime(options.selfVersion, options.selfCommand, options.deps));
438
+ if (!hasCandidate()) return null;
439
+ const installedServiceOwnsCurrentHome = options.deps?.installedServiceOwnsCurrentHome
440
+ ?? serviceManagerOwnsCurrentHome;
441
+ // Bun may populate the live environment from a project-controlled dotenv file. Markers
442
+ // therefore identify the service protocol but are not authority to execute census paths:
443
+ // require a live manager registration whose generated definition names this home first.
444
+ if (!installedServiceOwnsCurrentHome()) return null;
400
445
  return deferToNewerServiceRuntime(options.selfVersion, options.selfCommand, options.port, { env: options.env, ...options.deps });
401
446
  }
402
447
 
448
+ /**
449
+ * A claim's null home key means the definition omitted it, which only happens for a
450
+ * default-home install. Such a claim authorizes nothing when the current process resolved
451
+ * a non-default home — otherwise a project .env setting the env var plus the service marker
452
+ * would borrow a default install's registration for a project-chosen census directory.
453
+ */
454
+ export function serviceClaimMatchesCurrentHomes(
455
+ claim: ServiceManagerClaim,
456
+ current: { codexHome: string; opencodexHome: string },
457
+ env: NodeJS.ProcessEnv = process.env,
458
+ ): boolean {
459
+ return (claim.homes.codexHome === null
460
+ ? !env.CODEX_HOME?.trim()
461
+ : compareServicePathToInstall(claim.homes.codexHome, current.codexHome) === "same")
462
+ && (claim.homes.opencodexHome === null
463
+ ? !env.OPENCODEX_HOME?.trim()
464
+ : compareServicePathToInstall(claim.homes.opencodexHome, current.opencodexHome) === "same");
465
+ }
466
+
467
+ export interface ServiceOwnershipGateDeps {
468
+ readonly current?: { codexHome: string; opencodexHome: string };
469
+ readonly env?: NodeJS.ProcessEnv;
470
+ /** Test seam for the read-only manager inspection of the current config dir. */
471
+ readonly inspect?: (configDir: string) => ServiceManagerInstallation;
472
+ /** Test seam for the registered WinSW BINARY_PATH_NAME; production asks trusted `sc.exe qc`. */
473
+ readonly winswBinaryPathName?: () => string | null;
474
+ }
475
+
476
+ /**
477
+ * WinSW reports machine-wide SCM registration while its definition is read from the
478
+ * current config dir, so the delegation gate binds the registration to that definition:
479
+ * the registered service binary must be the claim's own executable. Any query failure,
480
+ * missing value, or mismatch refuses delegation. Other backends are unchanged.
481
+ */
482
+ export function serviceManagerOwnsCurrentHome(deps: ServiceOwnershipGateDeps = {}): boolean {
483
+ const current = deps.current ?? currentServiceHomes();
484
+ const inspect = deps.inspect ?? (configDir => inspectServiceManagerInstallation({ configDir }));
485
+ const installation = inspect(current.opencodexHome);
486
+ if (installation.kind !== "present") return false;
487
+ return installation.claims.some(claim => claim.registration === "present"
488
+ && serviceClaimMatchesCurrentHomes(claim, current, deps.env)
489
+ && (claim.backend !== "winsw" || winswRegistrationRunsDefinition(claim, deps)));
490
+ }
491
+
492
+ function winswRegistrationRunsDefinition(claim: ServiceManagerClaim, deps: ServiceOwnershipGateDeps): boolean {
493
+ const binaryPathName = (deps.winswBinaryPathName ?? (() => queryWinswBinaryPathName()))();
494
+ return binaryPathName !== null && scBinaryPathNamesExecutable(binaryPathName, claim.definitionPath);
495
+ }
496
+
403
497
  /**
404
498
  * A child a service manager started, as its environment proves it. launchd, systemd, and
405
499
  * current or repaired WinSW definitions write `OCX_SERVICE_MANAGED=1`; the Windows Task