@bitkyc08/opencodex 2.39.0 → 2.40.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 (144) hide show
  1. package/README.md +49 -21
  2. package/gui/dist/assets/index-BHe2rl_C.js +112 -0
  3. package/gui/dist/assets/index-CJSb3HPe.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/cursor/catalog.ts +140 -7
  7. package/src/adapters/cursor/discovery.ts +79 -87
  8. package/src/adapters/cursor/effort-map.ts +11 -0
  9. package/src/adapters/cursor/request-builder.ts +31 -4
  10. package/src/adapters/cursor.ts +17 -0
  11. package/src/adapters/openai-chat.ts +95 -9
  12. package/src/adapters/openai-responses.ts +73 -9
  13. package/src/bridge.ts +58 -13
  14. package/src/claude/auth-mode.ts +6 -9
  15. package/src/claude/gateway-cache.ts +23 -3
  16. package/src/claude/model-info.ts +13 -2
  17. package/src/cli/access.ts +30 -0
  18. package/src/cli/account-extended.ts +19 -10
  19. package/src/cli/capabilities.ts +40 -1
  20. package/src/cli/claude-agent-startup-sync.ts +3 -0
  21. package/src/cli/claude.ts +134 -36
  22. package/src/cli/connect.ts +226 -0
  23. package/src/cli/dispatch.ts +126 -21
  24. package/src/cli/gui-pair-client.ts +170 -0
  25. package/src/cli/gui.ts +87 -0
  26. package/src/cli/help.ts +4 -1
  27. package/src/cli/index.ts +76 -22
  28. package/src/cli/models-runtime-subcommands.ts +34 -0
  29. package/src/cli/models-runtime.ts +4 -0
  30. package/src/cli/models.ts +2 -1
  31. package/src/cli/opencode.ts +2 -1
  32. package/src/cli/provider-runtime.ts +31 -1
  33. package/src/cli/registry.ts +32 -2
  34. package/src/cli/runtime-api.ts +10 -1
  35. package/src/cli/status.ts +26 -0
  36. package/src/cli/system-command.ts +9 -3
  37. package/src/client/connect.ts +650 -0
  38. package/src/client/hub-client.ts +457 -0
  39. package/src/client/hub-relay.ts +288 -0
  40. package/src/client/machine-api.ts +139 -0
  41. package/src/client/machine-auth.ts +54 -0
  42. package/src/client/machine-listener.ts +143 -0
  43. package/src/client/runtime.ts +93 -0
  44. package/src/client/state.ts +175 -0
  45. package/src/clients/config-export.ts +16 -5
  46. package/src/codex/auth-api.ts +169 -15
  47. package/src/codex/auth-context.ts +61 -6
  48. package/src/codex/catalog/aggregation.ts +17 -13
  49. package/src/codex/catalog/metadata.ts +28 -0
  50. package/src/codex/catalog/provider-fetch.ts +38 -5
  51. package/src/codex/catalog.ts +1 -1
  52. package/src/codex/desired-state.ts +13 -3
  53. package/src/codex/inject.ts +212 -31
  54. package/src/codex/journal.ts +54 -6
  55. package/src/codex/main-account.ts +105 -33
  56. package/src/codex/native-main-claim.ts +23 -1
  57. package/src/codex/native-residue.ts +7 -0
  58. package/src/codex/reset-credit-auto-redeem.ts +237 -0
  59. package/src/combos/request.ts +15 -3
  60. package/src/combos/types.ts +12 -0
  61. package/src/config/paths.ts +15 -1
  62. package/src/config/provider-validation.ts +40 -0
  63. package/src/config.ts +447 -3
  64. package/src/generated/compatibility-version.json +224 -108
  65. package/src/images/artifacts.ts +73 -37
  66. package/src/images/fulfill.ts +6 -1
  67. package/src/images/index.ts +1 -1
  68. package/src/images/plan.ts +16 -2
  69. package/src/images/synthetic-tool.ts +5 -0
  70. package/src/images/xai-client.ts +34 -1
  71. package/src/integrations/cursor-detect.ts +133 -0
  72. package/src/integrations/cursor-seen.ts +31 -0
  73. package/src/lib/errors.ts +18 -0
  74. package/src/lib/gui-pair-capability.ts +104 -0
  75. package/src/lib/lab-live-route-production.ts +2 -1
  76. package/src/lib/process-control.ts +4 -3
  77. package/src/lib/service-secrets.ts +161 -1
  78. package/src/lib/windows-system-proxy.ts +115 -0
  79. package/src/oauth/index.ts +3 -2
  80. package/src/oauth/pool-settings-capability.ts +55 -0
  81. package/src/providers/api-keys.ts +3 -1
  82. package/src/providers/codex-capacity.ts +34 -4
  83. package/src/providers/derive.ts +10 -0
  84. package/src/providers/fastwire.ts +10 -2
  85. package/src/providers/key-store.ts +197 -0
  86. package/src/providers/model-rename-migration.ts +3 -0
  87. package/src/providers/openai-sidecar.ts +3 -3
  88. package/src/providers/quota.ts +91 -42
  89. package/src/providers/registry.ts +33 -3
  90. package/src/reasoning-effort.ts +32 -0
  91. package/src/remote/protocol.ts +109 -0
  92. package/src/responses/citation-markers.ts +101 -0
  93. package/src/responses/parser.ts +43 -3
  94. package/src/responses/spill-store.ts +9 -4
  95. package/src/router.ts +72 -8
  96. package/src/server/auth-cors.ts +37 -1
  97. package/src/server/gui-session.ts +449 -0
  98. package/src/server/gui-static.ts +24 -7
  99. package/src/server/images.ts +237 -10
  100. package/src/server/index.ts +356 -30
  101. package/src/server/management/agent-settings-routes.ts +7 -1
  102. package/src/server/management/api-key-rotation.ts +74 -0
  103. package/src/server/management/combo-routes.ts +18 -6
  104. package/src/server/management/config-routes.ts +22 -3
  105. package/src/server/management/context.ts +5 -1
  106. package/src/server/management/cursor-integration-routes.ts +98 -0
  107. package/src/server/management/logs-usage-routes.ts +2 -1
  108. package/src/server/management/model-routes.ts +79 -1
  109. package/src/server/management/model-rows.ts +23 -1
  110. package/src/server/management/oauth-account-routes.ts +135 -2
  111. package/src/server/management/provider-routes.ts +143 -9
  112. package/src/server/management/route-registry.ts +10 -0
  113. package/src/server/management/session-routes.ts +13 -0
  114. package/src/server/management/system-routes.ts +13 -1
  115. package/src/server/management-api.ts +8 -3
  116. package/src/server/management-auth.ts +124 -82
  117. package/src/server/models-capabilities.ts +124 -0
  118. package/src/server/ports.ts +7 -0
  119. package/src/server/proxy-liveness.ts +7 -0
  120. package/src/server/request-log.ts +11 -5
  121. package/src/server/responses/agent-task-recovery.ts +1 -0
  122. package/src/server/responses/codex-auth-error.ts +3 -1
  123. package/src/server/responses/collaboration.ts +26 -1
  124. package/src/server/responses/combo-stream-preflight.ts +25 -6
  125. package/src/server/responses/compact.ts +32 -12
  126. package/src/server/responses/context-overflow.ts +49 -0
  127. package/src/server/responses/core.ts +196 -12
  128. package/src/server/responses/fetch-helpers.ts +2 -1
  129. package/src/server/responses/outbound-body-guard.ts +110 -0
  130. package/src/server/responses/ws-upstream.ts +87 -6
  131. package/src/server/responses-self-named-namespace-scrub.ts +181 -0
  132. package/src/server/system-env.ts +78 -25
  133. package/src/service.ts +218 -42
  134. package/src/types/config.ts +134 -0
  135. package/src/types/provider.ts +54 -1
  136. package/src/types/request.ts +2 -0
  137. package/src/types.ts +5 -0
  138. package/src/update/job.ts +33 -5
  139. package/src/usage/cost.ts +32 -1
  140. package/src/usage/expected-prices.ts +16 -0
  141. package/src/usage/log.ts +6 -2
  142. package/src/usage/summary.ts +48 -6
  143. package/gui/dist/assets/index-D-lchsPw.js +0 -112
  144. package/gui/dist/assets/index-uvENYLin.css +0 -1
@@ -139,6 +139,70 @@ export interface InjectCodexOptions {
139
139
  * provider discovery so a deterministic config refusal cannot degrade an existing catalog.
140
140
  */
141
141
  validateOnly?: boolean;
142
+ /** Explicit remote routing target. Absence preserves byte-compatible standalone output. */
143
+ routingTarget?: CodexRoutingTarget;
144
+ journalOwner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
145
+ }
146
+
147
+ export interface CodexRoutingTarget {
148
+ baseUrl: string;
149
+ requiresAdmissionToken: boolean;
150
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN";
151
+ /**
152
+ * Opt-in authless Codex Desktop mode (#1107): inject the dedicated provider table with
153
+ * `requires_openai_auth = false` so Desktop skips the ChatGPT login gate. Only ever true for
154
+ * loopback targets that need no admission token; non-loopback admission is a separate layer
155
+ * and is never weakened by this flag.
156
+ */
157
+ desktopAuthless?: boolean;
158
+ }
159
+
160
+ function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTarget {
161
+ let parsed: URL;
162
+ try {
163
+ parsed = new URL(target.baseUrl);
164
+ } catch {
165
+ throw new TypeError("Codex routing target must be an absolute HTTP(S) /v1 URL");
166
+ }
167
+ if (
168
+ (parsed.protocol !== "http:" && parsed.protocol !== "https:")
169
+ || parsed.username
170
+ || parsed.password
171
+ || parsed.pathname !== "/v1"
172
+ || parsed.search
173
+ || parsed.hash
174
+ || target.tokenEnv !== "OPENCODEX_API_AUTH_TOKEN"
175
+ ) {
176
+ throw new TypeError("Codex routing target must be a canonical HTTP(S) /v1 URL without credentials, query, or fragment");
177
+ }
178
+ return { ...target, baseUrl: `${parsed.origin}/v1` };
179
+ }
180
+
181
+ /** Provider-table form is used for non-loopback admission and for the authless Desktop opt-in. */
182
+ function usesProviderTable(target: CodexRoutingTarget): boolean {
183
+ return target.requiresAdmissionToken || target.desktopAuthless === true;
184
+ }
185
+
186
+ export function standaloneCodexRoutingTarget(
187
+ port: number,
188
+ config?: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless">,
189
+ ): CodexRoutingTarget {
190
+ const loopback = config?.unauthenticatedLoopbackListener;
191
+ const effectivePort = loopback?.enabled ? loopback.port : port;
192
+ const hostname = loopback?.enabled ? undefined : config?.hostname;
193
+ const requiresAdmissionToken = loopback?.enabled ? false : shouldInjectApiAuthHeader(config);
194
+ return {
195
+ baseUrl: `http://${providerBaseHost(hostname)}:${effectivePort}/v1`,
196
+ requiresAdmissionToken,
197
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
198
+ ...(config?.codexDesktopAuthless === true && !requiresAdmissionToken
199
+ ? { desktopAuthless: true }
200
+ : {}),
201
+ };
202
+ }
203
+
204
+ function routingTargetOrigin(target: CodexRoutingTarget): string {
205
+ return target.baseUrl.slice(0, -3);
142
206
  }
143
207
 
144
208
  function configuredManagedSubagentDefaults(
@@ -213,28 +277,52 @@ export function shouldInjectApiAuthHeader(
213
277
 
214
278
  export function buildProviderTableBlock(
215
279
  port: number,
280
+ supportsWebsockets?: boolean,
281
+ includeApiAuthHeader?: boolean,
282
+ hostname?: string,
283
+ ): string;
284
+ export function buildProviderTableBlock(
285
+ target: CodexRoutingTarget,
286
+ supportsWebsockets?: boolean,
287
+ ): string;
288
+ export function buildProviderTableBlock(
289
+ portOrTarget: number | CodexRoutingTarget,
216
290
  supportsWebsockets = false,
217
291
  includeApiAuthHeader = false,
218
292
  hostname?: string,
219
293
  ): string {
220
- const host = providerBaseHost(hostname);
294
+ const target = typeof portOrTarget === "number"
295
+ ? validateCodexRoutingTarget({
296
+ baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
297
+ requiresAdmissionToken: includeApiAuthHeader,
298
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
299
+ })
300
+ : validateCodexRoutingTarget(portOrTarget);
301
+ return buildProviderTableBlockForTarget(target, supportsWebsockets);
302
+ }
303
+
304
+ function buildProviderTableBlockForTarget(
305
+ target: CodexRoutingTarget,
306
+ supportsWebsockets = false,
307
+ ): string {
221
308
  const lines = [
222
309
  "",
223
310
  OCX_SECTION_MARKER,
224
311
  "[model_providers.opencodex]",
225
312
  'name = "OpenCodex Proxy"',
226
- `base_url = "http://${host}:${port}/v1"`,
313
+ `base_url = ${tomlString(target.baseUrl)}`,
227
314
  'wire_api = "responses"',
228
- "requires_openai_auth = true",
315
+ // false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
316
+ `requires_openai_auth = ${target.desktopAuthless === true ? "false" : "true"}`,
229
317
  ];
230
- if (includeApiAuthHeader) {
318
+ if (target.requiresAdmissionToken) {
231
319
  // codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
232
320
  // hard-errors on a missing/empty variable instead of silently omitting auth. It
233
321
  // coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
234
322
  // login/account UX), and the server substitutes stored main auth for our admission
235
323
  // bearer (#1686), so the modern form is strictly better than the legacy
236
324
  // env_http_headers table this line used to emit.
237
- lines.push('env_key = "OPENCODEX_API_AUTH_TOKEN"');
325
+ lines.push(`env_key = ${tomlString(target.tokenEnv)}`);
238
326
  }
239
327
  if (supportsWebsockets) lines.push("supports_websockets = true");
240
328
  return lines.join("\n") + "\n";
@@ -243,8 +331,19 @@ export function buildProviderTableBlock(
243
331
  export function buildOpenaiBaseUrlLine(
244
332
  port: number,
245
333
  hostname?: string,
334
+ ): string;
335
+ export function buildOpenaiBaseUrlLine(target: CodexRoutingTarget): string;
336
+ export function buildOpenaiBaseUrlLine(
337
+ portOrTarget: number | CodexRoutingTarget,
338
+ hostname?: string,
246
339
  ): string {
247
- return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
340
+ return typeof portOrTarget === "number"
341
+ ? `openai_base_url = "http://${providerBaseHost(hostname)}:${portOrTarget}/v1"`
342
+ : buildOpenaiBaseUrlLineForTarget(validateCodexRoutingTarget(portOrTarget));
343
+ }
344
+
345
+ function buildOpenaiBaseUrlLineForTarget(target: CodexRoutingTarget): string {
346
+ return `openai_base_url = ${tomlString(target.baseUrl)}`;
248
347
  }
249
348
 
250
349
  /**
@@ -257,11 +356,23 @@ export function setRootOpenaiBaseUrl(
257
356
  content: string,
258
357
  port: number,
259
358
  hostname?: string,
359
+ ): { content: string; keptUserBaseUrl: boolean };
360
+ export function setRootOpenaiBaseUrl(
361
+ content: string,
362
+ target: CodexRoutingTarget,
363
+ ): { content: string; keptUserBaseUrl: boolean };
364
+ export function setRootOpenaiBaseUrl(
365
+ content: string,
366
+ portOrTarget: number | CodexRoutingTarget,
367
+ hostname?: string,
260
368
  ): { content: string; keptUserBaseUrl: boolean } {
369
+ if (typeof portOrTarget !== "number") {
370
+ return setRootOpenaiBaseUrlForTarget(content, validateCodexRoutingTarget(portOrTarget));
371
+ }
261
372
  const lines = content.split("\n");
262
373
  const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
263
374
  const rootEnd = firstTable === -1 ? lines.length : firstTable;
264
- const key = buildOpenaiBaseUrlLine(port, hostname);
375
+ const key = buildOpenaiBaseUrlLine(portOrTarget, hostname);
265
376
 
266
377
  for (let i = 0; i < rootEnd; i++) {
267
378
  if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
@@ -289,6 +400,33 @@ export function setRootOpenaiBaseUrl(
289
400
  return { content: lines.join("\n"), keptUserBaseUrl: false };
290
401
  }
291
402
 
403
+ function setRootOpenaiBaseUrlForTarget(
404
+ content: string,
405
+ target: CodexRoutingTarget,
406
+ ): { content: string; keptUserBaseUrl: boolean } {
407
+ const lines = content.split("\n");
408
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
409
+ const rootEnd = firstTable === -1 ? lines.length : firstTable;
410
+ const key = buildOpenaiBaseUrlLineForTarget(target);
411
+ for (let index = 0; index < rootEnd; index += 1) {
412
+ if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
413
+ const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
414
+ if (!markerOwned) return { content, keptUserBaseUrl: true };
415
+ lines[index] = key;
416
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
417
+ }
418
+ if (firstTable === -1) {
419
+ return {
420
+ content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
421
+ keptUserBaseUrl: false,
422
+ };
423
+ }
424
+ let insertAt = firstTable;
425
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
426
+ lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
427
+ return { content: lines.join("\n"), keptUserBaseUrl: false };
428
+ }
429
+
292
430
  /**
293
431
  * Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
294
432
  * A user's own root override (no marker) survives; an orphaned marker with no key line after
@@ -619,17 +757,48 @@ function stripOpencodexCatalogPath(content: string): string {
619
757
  .join("\n");
620
758
  }
621
759
 
622
- export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
623
- const host = providerBaseHost(hostname);
760
+ export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets?: boolean, includeApiAuthHeader?: boolean, hostname?: string, fastMode?: boolean): string;
761
+ export function buildProfileFile(target: CodexRoutingTarget, catalogPath?: string | null, supportsWebsockets?: boolean, fastMode?: boolean): string;
762
+ export function buildProfileFile(
763
+ portOrTarget: number | CodexRoutingTarget,
764
+ catalogPath?: string | null,
765
+ supportsWebsockets = false,
766
+ includeApiAuthHeaderOrFastMode?: boolean,
767
+ hostname?: string,
768
+ fastMode?: boolean,
769
+ ): string {
770
+ const target = typeof portOrTarget === "number"
771
+ ? validateCodexRoutingTarget({
772
+ baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
773
+ requiresAdmissionToken: includeApiAuthHeaderOrFastMode === true,
774
+ tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
775
+ })
776
+ : validateCodexRoutingTarget(portOrTarget);
777
+ return buildProfileFileForTarget(
778
+ target,
779
+ catalogPath,
780
+ supportsWebsockets,
781
+ typeof portOrTarget === "number" ? fastMode : includeApiAuthHeaderOrFastMode,
782
+ );
783
+ }
784
+
785
+ function buildProfileFileForTarget(
786
+ target: CodexRoutingTarget,
787
+ catalogPath?: string | null,
788
+ supportsWebsockets = false,
789
+ fastMode?: boolean,
790
+ ): string {
791
+ const origin = routingTargetOrigin(target);
792
+ const host = new URL(origin).host;
624
793
  // Design B (loopback): the reference/fallback file documents the root override form.
625
794
  // Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
626
- // the x-opencodex-api-key env header).
627
- if (!includeApiAuthHeader) {
795
+ // the x-opencodex-api-key env header); the authless Desktop opt-in shares that shape.
796
+ if (!usesProviderTable(target)) {
628
797
  const lines = [
629
798
  "# OpenCodex proxy fallback config (Design B)",
630
- `# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
799
+ `# Root override that points Codex's built-in openai provider at the proxy on ${host}.`,
631
800
  "# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
632
- buildOpenaiBaseUrlLine(port, hostname),
801
+ buildOpenaiBaseUrlLineForTarget(target),
633
802
  ];
634
803
  if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
635
804
  if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
@@ -637,12 +806,12 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
637
806
  }
638
807
  const lines = [
639
808
  "# OpenCodex proxy profile — use with: codex --profile opencodex",
640
- `# Routes all model requests through the opencodex proxy at ${host}:${port}`,
809
+ `# Routes all model requests through the opencodex proxy at ${host}`,
641
810
  'model_provider = "opencodex"',
642
811
  ];
643
812
  if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
644
813
  if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
645
- lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
814
+ lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
646
815
  return lines.join("\n");
647
816
  }
648
817
 
@@ -684,8 +853,14 @@ export async function injectCodexConfig(
684
853
  //
685
854
  // The listener port is fixed in config, never OS-assigned, so this value survives restarts
686
855
  // and matches what an already-running app-server read at startup.
687
- const loopback = config?.unauthenticatedLoopbackListener;
688
- if (loopback?.enabled) port = loopback.port;
856
+ let routingTarget: CodexRoutingTarget;
857
+ try {
858
+ routingTarget = options.routingTarget
859
+ ? validateCodexRoutingTarget(options.routingTarget)
860
+ : standaloneCodexRoutingTarget(port, config);
861
+ } catch (error) {
862
+ return { success: false, message: error instanceof Error ? error.message : "Invalid Codex routing target" };
863
+ }
689
864
  if (!existsSync(CODEX_CONFIG_PATH)) {
690
865
  return {
691
866
  success: false,
@@ -712,8 +887,8 @@ export async function injectCodexConfig(
712
887
  message:
713
888
  `⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
714
889
  ` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
715
- ` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
716
- `${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
890
+ ` Configure that provider for Responses passthrough at ${routingTarget.baseUrl}` +
891
+ `${routingTarget.requiresAdmissionToken ? ` with x-opencodex-api-key from ${routingTarget.tokenEnv}` : ""}.\n` +
717
892
  ` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
718
893
  };
719
894
  }
@@ -781,28 +956,26 @@ export async function injectCodexConfig(
781
956
  ? setRootModelCatalogPath(content, catalogPath)
782
957
  : stripOpencodexCatalogPath(content);
783
958
 
784
- const legacyMode = shouldInjectApiAuthHeader(config);
959
+ // Provider-table form: non-loopback admission (legacy) or the authless Desktop opt-in (#1107).
960
+ const legacyMode = usesProviderTable(routingTarget);
785
961
  let keptUserBaseUrl = false;
786
962
  if (legacyMode) {
787
963
  // Legacy (non-loopback) injection: the built-in openai provider cannot carry the
788
964
  // x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
965
+ // The authless opt-in needs the same table because only a dedicated provider can carry
966
+ // requires_openai_auth = false.
789
967
  // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
790
968
  content = setRootModelProvider(content);
791
969
  // 2) Provider table appended at EOF (position-independent).
792
970
  content =
793
971
  content.trimEnd() +
794
972
  "\n" +
795
- buildProviderTableBlock(
796
- port,
797
- websocketsEnabled(config ?? {}),
798
- true,
799
- config?.hostname,
800
- );
973
+ buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
801
974
  } else {
802
975
  // Design B (loopback): a single root override; codex keeps its native `openai` provider id
803
976
  // so thread history is never remapped. Any legacy form was already stripped above.
804
977
  content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
805
- const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
978
+ const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
806
979
  content = result.content;
807
980
  keptUserBaseUrl = result.keptUserBaseUrl;
808
981
  }
@@ -838,7 +1011,12 @@ export async function injectCodexConfig(
838
1011
  managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
839
1012
  }
840
1013
 
841
- const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
1014
+ const profileContent = buildProfileFileForTarget(
1015
+ routingTarget,
1016
+ catalogPath,
1017
+ websocketsEnabled(config ?? {}),
1018
+ config?.fastMode,
1019
+ );
842
1020
  content = applyEol(content, eol);
843
1021
 
844
1022
  /*
@@ -916,6 +1094,7 @@ export async function injectCodexConfig(
916
1094
  writeJournal({
917
1095
  currentStateIsNative: !hasInjectedCodexRouting(rawContent),
918
1096
  configContent: baselineContent,
1097
+ owner: options.journalOwner,
919
1098
  });
920
1099
  atomicWriteFile(CODEX_CONFIG_PATH, content);
921
1100
  atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
@@ -1118,9 +1297,11 @@ export async function injectCodexConfig(
1118
1297
  ` Reference config: ${CODEX_PROFILE_PATH}`,
1119
1298
  };
1120
1299
  }
1121
- const headline = legacyMode
1122
- ? `Injected opencodex as default provider into Codex config.\n`
1123
- : `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
1300
+ const headline = routingTarget.desktopAuthless === true
1301
+ ? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n`
1302
+ : legacyMode
1303
+ ? `Injected opencodex as default provider into Codex config.\n`
1304
+ : `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url).\n`;
1124
1305
  return {
1125
1306
  success: true,
1126
1307
  ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
@@ -16,6 +16,10 @@ import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
16
16
  */
17
17
  export const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
18
18
 
19
+ export type JournalOwner =
20
+ | { kind: "process"; pid: number }
21
+ | { kind: "client"; apiKeyId: string };
22
+
19
23
  interface Journal {
20
24
  version: 1;
21
25
  originalConfig: string;
@@ -41,10 +45,11 @@ interface Journal {
41
45
  */
42
46
  injectedCatalogPath?: string | null;
43
47
  pid: number;
48
+ owner?: JournalOwner;
44
49
  timestamp: string;
45
50
  }
46
51
 
47
- interface RestoreJournalResult {
52
+ export interface RestoreJournalResult {
48
53
  configRestored: boolean;
49
54
  profileRestored: boolean;
50
55
  configChanged: boolean;
@@ -71,6 +76,7 @@ export interface WriteJournalOptions {
71
76
  * another process rewrites config.toml mid-flight.
72
77
  */
73
78
  configContent?: string;
79
+ owner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
74
80
  }
75
81
 
76
82
  /**
@@ -103,6 +109,9 @@ export function writeJournal(options: WriteJournalOptions = {}): void {
103
109
  originalConfig: Buffer.from(config).toString("base64"),
104
110
  originalProfile: profile ? Buffer.from(profile).toString("base64") : null,
105
111
  pid: process.pid,
112
+ owner: options.owner?.kind === "client"
113
+ ? { kind: "client", apiKeyId: options.owner.apiKeyId }
114
+ : { kind: "process", pid: process.pid },
106
115
  timestamp: new Date().toISOString(),
107
116
  };
108
117
  atomicWriteFile(JOURNAL_PATH, JSON.stringify(journal));
@@ -168,6 +177,20 @@ function readJournal(): Journal | null {
168
177
  }
169
178
  }
170
179
 
180
+ export function journalOwner(): JournalOwner | null {
181
+ const journal = readJournal();
182
+ if (!journal) return null;
183
+ if (journal.owner?.kind === "client" && typeof journal.owner.apiKeyId === "string" && journal.owner.apiKeyId) {
184
+ return { kind: "client", apiKeyId: journal.owner.apiKeyId };
185
+ }
186
+ if (journal.owner?.kind === "process" && Number.isSafeInteger(journal.owner.pid) && journal.owner.pid > 0) {
187
+ return { kind: "process", pid: journal.owner.pid };
188
+ }
189
+ return Number.isSafeInteger(journal.pid) && journal.pid > 0
190
+ ? { kind: "process", pid: journal.pid }
191
+ : null;
192
+ }
193
+
171
194
  export function restoreJournalState(): RestoreJournalResult {
172
195
  const journal = readJournal();
173
196
  if (!journal) {
@@ -187,10 +210,22 @@ export function restoreJournalState(): RestoreJournalResult {
187
210
  if (profileUnchanged) {
188
211
  if (journal.originalProfile !== null) {
189
212
  atomicWriteFile(CODEX_PROFILE_PATH, Buffer.from(journal.originalProfile, "base64").toString("utf-8"));
213
+ profileRestored = true;
190
214
  } else if (existsSync(CODEX_PROFILE_PATH)) {
191
- try { unlinkSync(CODEX_PROFILE_PATH); } catch { /* ignore */ }
215
+ // "There was no profile before, so remove the one we generated." Claiming success
216
+ // without checking is how a caller ends up deleting the journal, reporting a clean
217
+ // restore, and leaving our profile on disk with nothing left that records it should
218
+ // not be there. ENOENT is the one benign outcome: the file is already gone, which is
219
+ // the state we wanted.
220
+ try {
221
+ unlinkSync(CODEX_PROFILE_PATH);
222
+ profileRestored = true;
223
+ } catch (error) {
224
+ profileRestored = (error as NodeJS.ErrnoException).code === "ENOENT";
225
+ }
226
+ } else {
227
+ profileRestored = true;
192
228
  }
193
- profileRestored = true;
194
229
  }
195
230
  const complete = configRestored && profileRestored;
196
231
  if (complete) removeJournal();
@@ -207,11 +242,24 @@ export function restoreJournal(): boolean {
207
242
  return restoreJournalState().complete;
208
243
  }
209
244
 
210
- export function reconcileJournal(): boolean {
245
+ export interface ReconcileJournalOptions {
246
+ activeClientApiKeyId?: string;
247
+ }
248
+
249
+ export function reconcileJournal(options: ReconcileJournalOptions = {}): boolean {
211
250
  const journal = readJournal();
212
251
  if (!journal) return false;
252
+ const owner = journalOwner();
253
+ if (owner?.kind === "client") {
254
+ if (options.activeClientApiKeyId === owner.apiKeyId) return false;
255
+ const restored = restoreJournalState();
256
+ if (!restored.configRestored && !restored.profileRestored) return false;
257
+ console.error(`⚠️ Uncommitted or mismatched client routing (${owner.apiKeyId}) was restored from the Codex journal.`);
258
+ return true;
259
+ }
260
+ const pid = owner?.kind === "process" ? owner.pid : journal.pid;
213
261
  try {
214
- process.kill(journal.pid, 0);
262
+ process.kill(pid, 0);
215
263
  return false;
216
264
  } catch (e: unknown) {
217
265
  if ((e as NodeJS.ErrnoException).code === "EPERM") {
@@ -220,6 +268,6 @@ export function reconcileJournal(): boolean {
220
268
  }
221
269
  const restored = restoreJournalState();
222
270
  if (!restored.configRestored && !restored.profileRestored) return false;
223
- console.error(`⚠️ Previous session (PID ${journal.pid}) did not shut down cleanly. Codex state restored from journal.`);
271
+ console.error(`⚠️ Previous session (PID ${pid}) did not shut down cleanly. Codex state restored from journal.`);
224
272
  return true;
225
273
  }