@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CODE_MODE_EXEC_TOOL_NAME,
3
+ dottedToolName,
3
4
  namespacedToolName,
4
5
  normalizeDeclaredToolName,
5
6
  } from "../types";
@@ -74,16 +75,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
74
75
  return !!value && typeof value === "object" && !Array.isArray(value);
75
76
  }
76
77
 
77
- function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
78
- if (!isPlainObject(tool)) return;
78
+ /**
79
+ * A dotted spelling is a safe alias only when it cannot ALSO be read as some other identity's
80
+ * canonical `ns__name`.
81
+ *
82
+ * `{namespace: "x__y", name: "z"}` produces the dotted spelling "x__y.z", which is exactly the
83
+ * canonical wire name of `{namespace: "x", name: "y.z"}`. If only the latter is declared, an
84
+ * echoed call for the former would still find "x__y.z" in the declared set and be authorized as
85
+ * a tool the caller never granted. Requiring both halves to be free of the `__` separator keeps
86
+ * a dotted alias from ever impersonating a canonical name.
87
+ */
88
+ function dottedAliasIsUnambiguous(namespace: string, name: string): boolean {
89
+ return !namespace.includes("__") && !name.includes("__");
90
+ }
91
+
92
+ function wireToolInnerName(tool: unknown): string | undefined {
93
+ if (!isPlainObject(tool)) return undefined;
79
94
  const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
80
95
  ? tool.function
81
96
  : undefined;
82
- const name = typeof tool.name === "string" && tool.name.length > 0
97
+ return typeof tool.name === "string" && tool.name.length > 0
83
98
  ? tool.name
84
99
  : typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
85
100
  ? nestedFunction.name
86
101
  : undefined;
102
+ }
103
+
104
+ function addWireToolName(
105
+ names: Set<string>,
106
+ tool: unknown,
107
+ namespace?: string,
108
+ ambiguousDottedAliases?: ReadonlySet<string>,
109
+ ): void {
110
+ if (!isPlainObject(tool)) return;
111
+ const name = wireToolInnerName(tool);
87
112
  if (!name) return;
88
113
  // Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
89
114
  // as a bare inner name or as the flattened form; accept both rather than guess which
@@ -93,6 +118,17 @@ function addWireToolName(names: Set<string>, tool: unknown, namespace?: string):
93
118
  return;
94
119
  }
95
120
  names.add(namespacedToolName(namespace, name));
121
+ // Some routed providers echo the flattened wire name with a dot (`ns.name`, observed with
122
+ // muse-spark via opencode-go) instead of `ns__name`. It is the same tool identity, so register
123
+ // the dotted spelling too, mirroring `toolChoiceAliases` (#3402) -- but only while that
124
+ // spelling names exactly one declared tool. Dots are legal inside both a namespace and a
125
+ // name, so two distinct identities can flatten onto one dotted alias; accepting it then would
126
+ // authorize a call the caller never declared under that identity. Ambiguous aliases fall back
127
+ // to the unambiguous `ns__name` form.
128
+ const dotted = dottedToolName(namespace, name);
129
+ if (dottedAliasIsUnambiguous(namespace, name) && !ambiguousDottedAliases?.has(dotted)) {
130
+ names.add(dotted);
131
+ }
96
132
  // `exec` is the one name that also switches on nested-helper normalization, so a bare alias
97
133
  // for a namespaced MCP tool would silently authorize `exec_command`/`shell_command`/
98
134
  // `apply_patch` the request never declared. Every other inner name keeps the bare alias.
@@ -118,19 +154,65 @@ export function currentTurnWireToolCatalogBody(
118
154
  return { ...body, input: body.input.slice(start) };
119
155
  }
120
156
 
121
- function addWireToolSpecs(names: Set<string>, specs: unknown): void {
157
+ function addWireToolSpecs(
158
+ names: Set<string>,
159
+ specs: unknown,
160
+ ambiguousDottedAliases?: ReadonlySet<string>,
161
+ ): void {
122
162
  if (!Array.isArray(specs)) return;
123
163
  for (const spec of specs) {
124
164
  if (!isPlainObject(spec)) continue;
125
165
  if (spec.type === "namespace" && Array.isArray(spec.tools)) {
126
166
  const namespace = typeof spec.name === "string" ? spec.name : undefined;
127
- for (const inner of spec.tools) addWireToolName(names, inner, namespace);
167
+ for (const inner of spec.tools) addWireToolName(names, inner, namespace, ambiguousDottedAliases);
128
168
  continue;
129
169
  }
130
- addWireToolName(names, spec);
170
+ addWireToolName(names, spec, undefined, ambiguousDottedAliases);
131
171
  }
132
172
  }
133
173
 
174
+ /**
175
+ * Dotted aliases that more than one declared identity would claim, plus dotted aliases that
176
+ * collide with a canonical or bare declared name.
177
+ *
178
+ * Resolved over the WHOLE catalog before any name is registered, so which identity "wins" can
179
+ * never depend on declaration order -- an order the caller controls.
180
+ */
181
+ function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): Set<string> {
182
+ const owners = new Map<string, string | null>();
183
+ const claim = (alias: string, identity: string): void => {
184
+ const owner = owners.get(alias);
185
+ if (owner === undefined) owners.set(alias, identity);
186
+ else if (owner !== identity) owners.set(alias, null);
187
+ };
188
+ for (const specs of specGroups) {
189
+ if (!Array.isArray(specs)) continue;
190
+ for (const spec of specs) {
191
+ if (!isPlainObject(spec)) continue;
192
+ if (spec.type === "namespace" && Array.isArray(spec.tools)) {
193
+ const namespace = typeof spec.name === "string" ? spec.name : undefined;
194
+ if (!namespace || namespace === BUILTIN_FUNCTIONS_NAMESPACE) continue;
195
+ for (const inner of spec.tools) {
196
+ const name = wireToolInnerName(inner);
197
+ if (!name) continue;
198
+ const identity = JSON.stringify([namespace, name]);
199
+ claim(dottedToolName(namespace, name), identity);
200
+ // A canonical or bare name already owned by a different identity poisons the dotted
201
+ // alias that would shadow it.
202
+ claim(namespacedToolName(namespace, name), identity);
203
+ claim(name, identity);
204
+ }
205
+ continue;
206
+ }
207
+ const name = wireToolInnerName(spec);
208
+ if (name) claim(name, JSON.stringify([undefined, name]));
209
+ }
210
+ }
211
+ const ambiguous = new Set<string>();
212
+ for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
213
+ return ambiguous;
214
+ }
215
+
134
216
  /**
135
217
  * Tool names the OUTBOUND Responses body actually declared.
136
218
  *
@@ -142,15 +224,17 @@ function addWireToolSpecs(names: Set<string>, specs: unknown): void {
142
224
  export function collectDeclaredWireToolNames(body: unknown): Set<string> {
143
225
  const names = new Set<string>();
144
226
  if (!isPlainObject(body)) return names;
145
- addWireToolSpecs(names, body.tools);
227
+ const specGroups: unknown[] = [body.tools];
146
228
  if (Array.isArray(body.input)) {
147
229
  for (const item of body.input) {
148
230
  if (
149
231
  isPlainObject(item)
150
232
  && (item.type === "additional_tools" || item.type === "tool_search_output")
151
- ) addWireToolSpecs(names, item.tools);
233
+ ) specGroups.push(item.tools);
152
234
  }
153
235
  }
236
+ const ambiguousDottedAliases = collectAmbiguousDottedAliases(specGroups);
237
+ for (const specs of specGroups) addWireToolSpecs(names, specs, ambiguousDottedAliases);
154
238
  return names;
155
239
  }
156
240
 
@@ -293,7 +377,15 @@ function undeclaredNameInItem(
293
377
  if (typeof item.namespace === "string") {
294
378
  // Namespaced calls are matched by their full wire name only — never legacy-normalize
295
379
  // them, or an undeclared namespaced `exec_command` could slip through as bare `exec`.
380
+ // Both flattened spellings (`ns__name` and the dotted `ns.name` some providers echo,
381
+ // #3402) name the same tool identity.
296
382
  if (declared.has(namespacedToolName(item.namespace, name))) return undefined;
383
+ // Only consult the dotted spelling when it cannot double as another identity's canonical
384
+ // name; otherwise a stranger's `ns__name` would authorize this call.
385
+ if (
386
+ dottedAliasIsUnambiguous(item.namespace, name)
387
+ && declared.has(dottedToolName(item.namespace, name))
388
+ ) return undefined;
297
389
  return name;
298
390
  }
299
391
  const effectiveName = normalizeDeclaredToolName(name, declared);
@@ -0,0 +1,23 @@
1
+ const SAFE_RESPONSE_HEADER_EXACT = new Set([
2
+ "retry-after", "x-request-id", "openai-request-id", "x-codex-turn-state",
3
+ "openai-model", "x-models-etag", "x-reasoning-included",
4
+ "x-codex-credits-has-credits", "x-codex-credits-unlimited", "x-codex-credits-balance",
5
+ "x-codex-promo-message", "x-codex-safety-buffering-enabled", "x-codex-safety-buffering-faster-model",
6
+ ]);
7
+
8
+ /** Response metadata may carry only the same non-credential headers as native WS errors. */
9
+ export function isSafeResponseHeader(name: string): boolean {
10
+ const lower = name.toLowerCase();
11
+ return SAFE_RESPONSE_HEADER_EXACT.has(lower)
12
+ || lower.startsWith("x-ratelimit-")
13
+ || /^x-codex(?:-[a-z0-9-]+)?-(primary|secondary|tertiary)-(used-percent|window-minutes|reset-at)$/.test(lower)
14
+ || /^x-codex(?:-[a-z0-9-]+)?-limit-name$/.test(lower);
15
+ }
16
+
17
+ export function safeResponseHeaders(headers: Headers): Record<string, string> {
18
+ const out: Record<string, string> = {};
19
+ for (const [name, value] of headers) {
20
+ if (isSafeResponseHeader(name)) out[name.toLowerCase()] = value;
21
+ }
22
+ return out;
23
+ }
@@ -19,6 +19,9 @@ import {
19
19
  CodexThreadAffinityExpiredError,
20
20
  } from "../codex/auth-context";
21
21
  import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
22
+ import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
23
+ import { isCodexReserveRequestEligible } from "../codex/loopback-target";
24
+ import type { DataPlaneAdmission } from "./auth-cors";
22
25
  import { formatCodexProviderForLog } from "../codex/routing";
23
26
  import { signalWithTimeout } from "../lib/abort";
24
27
  import { readBoundedResponseBytes } from "../lib/bounded-body";
@@ -52,6 +55,7 @@ export async function handleSearch(
52
55
  config: OcxConfig,
53
56
  logCtx: RequestLogContext,
54
57
  turnAdmissionLease?: AdmissionLease,
58
+ admission?: DataPlaneAdmission,
55
59
  ): Promise<Response> {
56
60
  try { validateForwardAdmissionCredential(req.headers, config); }
57
61
  catch (err) {
@@ -93,6 +97,10 @@ export async function handleSearch(
93
97
  }
94
98
  }
95
99
 
100
+ if (isCodexReserveRequestEligible(config, admission) && (exactAccount?.modelId ?? model) === NATIVE_RESERVE_MODEL) {
101
+ return formatErrorResponse(400, "invalid_request_error",
102
+ "Luna Reserve compatibility is only available as a conversation model, not the standalone search relay. Choose another search model.");
103
+ }
96
104
  const candidates = listOpenAiForwardSidecarCandidates(config);
97
105
  if (candidates.length === 0) {
98
106
  return formatErrorResponse(
@@ -107,6 +115,7 @@ export async function handleSearch(
107
115
  try {
108
116
  upstream = await resolveFirstUsableOpenAiSidecar(candidates, req.headers, config, {
109
117
  exactAccount,
118
+ admission,
110
119
  beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
111
120
  });
112
121
  if (!upstream) {
@@ -0,0 +1,27 @@
1
+ import { mutatePersistedConfig } from "../config";
2
+ import { migrateSubagentModels } from "../config/subagent-models";
3
+ import type { OcxConfig } from "../types";
4
+
5
+ /** Rebase the upgrade on disk so another startup cannot shift the roster twice. */
6
+ export function migrateStartupSubagentModels(config: OcxConfig): OcxConfig {
7
+ const projection = { ...config };
8
+ if (!migrateSubagentModels(projection)) return config;
9
+ try {
10
+ const outcome = mutatePersistedConfig(fresh => ({
11
+ changed: migrateSubagentModels(fresh),
12
+ value: fresh,
13
+ }));
14
+ if (outcome.status === "unavailable") {
15
+ console.warn(`[subagent-models-migration] Persistence unavailable (${outcome.reason}); using the upgraded roster in memory only.`);
16
+ } else {
17
+ // Called before live consumers are initialized: later startup saves must
18
+ // use the whole rebased document, not stale unrelated fields from loadConfig.
19
+ return outcome.value;
20
+ }
21
+ } catch {
22
+ // A contended coordinator or failed atomic write must not prevent proxy startup.
23
+ // Do not log the exception: filesystem errors may contain private paths.
24
+ console.warn("[subagent-models-migration] Persistence failed; using the upgraded roster in memory only.");
25
+ }
26
+ return projection;
27
+ }
@@ -0,0 +1,238 @@
1
+ import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
2
+ import { delimiter, join } from "node:path";
3
+ import { getConfigDir } from "../config";
4
+ import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
+ import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
6
+ import { resolveClaudeAuthMode } from "../claude/auth-mode";
7
+ import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
8
+ import type { OcxConfig } from "../types";
9
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
10
+
11
+ /**
12
+ * Does the opencodex dummy marker belong in the system environment?
13
+ *
14
+ * Keyed on the SAME resolver `ocx claude` uses, so an auto config with no Claude auth
15
+ * also reaches plain `claude` launches — before this, auto-absent users got nothing
16
+ * from auto-connect and the feature looked broken for exactly the people it helps
17
+ * (devlog 260726_claude_auth_auto/035).
18
+ *
19
+ * NOTE this is a SNAPSHOT: the file only changes when this runs (proxy start, `ocx
20
+ * ensure`, or a settings save). `ocx claude` re-resolves live on every launch.
21
+ */
22
+ export type SystemEnvDeps = {
23
+ /** Test seam; production uses the authenticated Node-launcher context. */
24
+ preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
25
+ /** Test seam for auth sources; `env` and `ownTokens` stay bound below. */
26
+ authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
27
+ };
28
+
29
+ /**
30
+ * Bun may synthesize Anthropic variables from a project `.env` before this module runs.
31
+ * Only values recorded by the plain-Node launcher are trusted as parent exports. Direct
32
+ * Bun/service launches have no proof-bound slot list, so they fail closed and let the
33
+ * file/keychain auth sources decide instead of allowing dotenv to select subscription mode.
34
+ */
35
+ function systemEnvAnthropicEnv(
36
+ env: NodeJS.ProcessEnv,
37
+ preBunAnthropicSlots: readonly AnthropicParentEnvSlot[] | null | undefined,
38
+ ): NodeJS.ProcessEnv {
39
+ const trustedSlots = preBunAnthropicSlots === undefined
40
+ ? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
41
+ : preBunAnthropicSlots ?? [];
42
+ const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
43
+ const sanitized = { ...env };
44
+ for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
45
+ if (sanitized[name] !== undefined && !exported.has(name)) delete sanitized[name];
46
+ }
47
+ return sanitized;
48
+ }
49
+
50
+ export function systemEnvMarkerMode(config: OcxConfig, deps: SystemEnvDeps = {}): "proxy" | "subscription" {
51
+ const env = systemEnvAnthropicEnv(process.env, deps.preBunAnthropicSlots);
52
+ const ownTokens = ownAdmissionTokens(config);
53
+ return resolveClaudeAuthMode(config, detectClaudeAuth({
54
+ ...defaultAuthDetectDeps(env, ownTokens),
55
+ ...(deps.authDetect ?? {}),
56
+ env: () => env,
57
+ ownTokens,
58
+ })).markerMode;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Shell-hook env file: written on inject, sourced by the shell hook in .zshrc.
63
+ // This works for ALL new shells immediately, unlike launchctl setenv which only
64
+ // reaches processes launched directly by launchd (not Terminal.app children).
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export function getShellEnvFilePath(): string {
68
+ return join(getConfigDir(), "claude-env.sh");
69
+ }
70
+
71
+ function shellValue(value: string): string {
72
+ return `'${value.replaceAll("'", `'\\''`)}'`;
73
+ }
74
+
75
+ export function writeShellEnvFile(
76
+ port: number,
77
+ config: OcxConfig,
78
+ modelEnv: Record<string, string> = {},
79
+ auto?: AutoContextMode,
80
+ deps: SystemEnvDeps = {},
81
+ ): void {
82
+ const lines = [
83
+ `# Generated by opencodex — do not edit manually`,
84
+ `export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`,
85
+ `export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`,
86
+ ];
87
+ // New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already
88
+ // exported in their shell wins even though launchctl knows nothing about it.
89
+ const conditional = (name: string, value: string) =>
90
+ `[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`;
91
+ if (systemEnvMarkerMode(config, deps) === "proxy") {
92
+ if (config.apiKeys?.length) {
93
+ lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`);
94
+ } else {
95
+ lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER));
96
+ }
97
+ }
98
+ // Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2).
99
+ if (modelEnv.ANTHROPIC_MODEL) {
100
+ lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`);
101
+ } else if (config.claudeCode?.model) {
102
+ lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`);
103
+ }
104
+ for (const [name, value] of Object.entries(modelEnv)) {
105
+ if (name === "ANTHROPIC_MODEL") continue;
106
+ lines.push(conditional(name, value));
107
+ }
108
+ const maxCtx = config.claudeCode?.maxContextTokens;
109
+ if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
110
+ lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx))));
111
+ lines.push(conditional("DISABLE_COMPACT", "1"));
112
+ }
113
+ // Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl.
114
+ const autoShell = auto ?? resolveAutoContext(config.claudeCode);
115
+ if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow)));
116
+ if (config.claudeCode?.alwaysEnableEffort === true) {
117
+ lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
118
+ }
119
+ const shellEnvPath = getShellEnvFilePath();
120
+ recordOwnedConfigPath(getConfigDir(), shellEnvPath);
121
+ mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
122
+ writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
123
+ }
124
+
125
+ export function removeShellEnvFile(): void {
126
+ try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // .zshrc hook auto-install: adds a one-liner that sources claude-env.sh.
131
+ // Idempotent — skips if the hook line already exists.
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const SHELL_HOOK_MARKER = "# opencodex claude-env hook";
135
+ const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`;
136
+
137
+ export function installShellHook(): { installed: boolean; reason?: string } {
138
+ if (process.platform !== "darwin") return { installed: false, reason: "not macOS" };
139
+ const home = process.env.HOME;
140
+ if (!home) return { installed: false, reason: "no HOME" };
141
+ const zshrcPath = join(home, ".zshrc");
142
+ try {
143
+ let content = "";
144
+ try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ }
145
+ if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" };
146
+ const addition = `\n${SHELL_HOOK_LINE}\n`;
147
+ writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 });
148
+ return { installed: true };
149
+ } catch (err) {
150
+ return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` };
151
+ }
152
+ }
153
+
154
+ export function uninstallShellHook(): { removed: boolean; reason?: string } {
155
+ if (process.platform !== "darwin") return { removed: false, reason: "not macOS" };
156
+ const home = process.env.HOME;
157
+ if (!home) return { removed: false, reason: "no HOME" };
158
+ const zshrcPath = join(home, ".zshrc");
159
+ try {
160
+ const content = readFileSync(zshrcPath, "utf8");
161
+ if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" };
162
+ // Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home
163
+ // directory an editor or another OS has touched — did not match, so the file was
164
+ // rewritten unchanged and the caller was told the hook was removed. Reporting success
165
+ // while the hook still sources on every new shell is the worse of the two failures.
166
+ const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n");
167
+ // Verify instead of assuming: if the marker survives, the block is shaped in a way this
168
+ // pattern does not own, and the honest answer is failure rather than a silent no-op.
169
+ if (cleaned.includes(SHELL_HOOK_MARKER)) {
170
+ return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" };
171
+ }
172
+ writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 });
173
+ return { removed: true };
174
+ } catch (error) {
175
+ if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") {
176
+ return { removed: false, reason: "not installed" };
177
+ }
178
+ return { removed: false, reason: "read/write failed" };
179
+ }
180
+ }
181
+
182
+ /** Whether a real `claude` executable is discoverable from this process's PATH. */
183
+ export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean {
184
+ if (!pathValue) return false;
185
+ for (const directory of pathValue.split(delimiter)) {
186
+ // An empty PATH segment means the current directory. Do not let the proxy treat a
187
+ // workspace-local file as a durable user installation.
188
+ if (!directory) continue;
189
+ const candidate = join(directory, "claude");
190
+ try {
191
+ if (!statSync(candidate).isFile()) continue;
192
+ accessSync(candidate, constants.X_OK);
193
+ return true;
194
+ } catch {
195
+ // Keep scanning PATH after missing, non-file, and non-executable entries.
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Keep the shell hook aligned with the integration that can actually consume it.
203
+ * Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists
204
+ * only for plain Claude Code CLI launches.
205
+ *
206
+ * Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered
207
+ * from the PATH of whichever process calls this. A launchd/service context with a stripped
208
+ * PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will
209
+ * remove the hook. That is the intended failure direction — removing an OpenCodex-owned block
210
+ * is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an
211
+ * uninstalled CLI is the stale state this reconciliation exists to clear. Only the block
212
+ * carrying our own marker is ever touched; user lines are preserved.
213
+ */
214
+ export function reconcileShellHook(systemEnvInjected: boolean): {
215
+ changed: boolean;
216
+ state: "installed" | "absent" | "failed";
217
+ reason?: string;
218
+ } {
219
+ if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" };
220
+ if (systemEnvInjected && claudeCodeCliInstalled()) {
221
+ const result = installShellHook();
222
+ if (result.installed) return { changed: true, state: "installed" };
223
+ if (result.reason === "already installed") {
224
+ return { changed: false, state: "installed", reason: result.reason };
225
+ }
226
+ return { changed: false, state: "failed", reason: result.reason ?? "install failed" };
227
+ }
228
+
229
+ const result = uninstallShellHook();
230
+ if (!result.removed && result.reason !== "not installed") {
231
+ return { changed: false, state: "failed", reason: result.reason ?? "remove failed" };
232
+ }
233
+ return {
234
+ changed: result.removed,
235
+ state: "absent",
236
+ reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive",
237
+ };
238
+ }