@yansigit/opencodex 2.36.1 → 2.36.2-dev.20260829.52

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 (77) hide show
  1. package/README.md +9 -4
  2. package/gui/dist/assets/{ApiKeys-CsmNyf4I.js → ApiKeys-GpvAosG-.js} +1 -1
  3. package/gui/dist/assets/{Claude-B4sHsGTD.js → Claude-Biju6rfh.js} +1 -1
  4. package/gui/dist/assets/{CodexSet-BuAQ4YQn.js → CodexSet-_EkE9hNC.js} +1 -1
  5. package/gui/dist/assets/{FileIntegrationPage-BC65sEaS.js → FileIntegrationPage-BL55KJCW.js} +1 -1
  6. package/gui/dist/assets/{Grok-DNJWlISf.js → Grok-Dp3sOS5B.js} +1 -1
  7. package/gui/dist/assets/{Integrations-vGJshFZK.js → Integrations-Bx0V4Dx1.js} +2 -2
  8. package/gui/dist/assets/{IntegrationsOverview-DYMkDFHE.js → IntegrationsOverview-CT41lCh4.js} +1 -1
  9. package/gui/dist/assets/{Logs-Coc7_RPP.js → Logs-BxS_u7Hr.js} +1 -1
  10. package/gui/dist/assets/{Models-CKVT8Meb.js → Models-DvaBTkUM.js} +1 -1
  11. package/gui/dist/assets/{NumberStepper-Dug4wR8n.js → NumberStepper-97eq4OKR.js} +1 -1
  12. package/gui/dist/assets/Providers-CB9I0Vv1.js +1 -0
  13. package/gui/dist/assets/{RestoreDialog-BpuvVU2w.js → RestoreDialog-C9sltYZD.js} +1 -1
  14. package/gui/dist/assets/{Startup-DAUQqNEN.js → Startup-BvFv8yC2.js} +1 -1
  15. package/gui/dist/assets/{Storage-DfP95SlJ.js → Storage-CKa2mqSI.js} +1 -1
  16. package/gui/dist/assets/{Subagents-CMNsZLfk.js → Subagents-B4oMNOVh.js} +1 -1
  17. package/gui/dist/assets/{Usage-ESFRfBMO.js → Usage-gaSKtbMw.js} +1 -1
  18. package/gui/dist/assets/{codex-stale-banner-W4FhikN7.js → codex-stale-banner-B5VQPO3t.js} +1 -1
  19. package/gui/dist/assets/{data-surface-B8PX8rjm.js → data-surface-B0APtUxm.js} +1 -1
  20. package/gui/dist/assets/{data-surface-CETFXqiA.js → data-surface-CH5yKPY1.js} +1 -1
  21. package/gui/dist/assets/index-C8flH881.css +1 -0
  22. package/gui/dist/assets/{index-DHQHBRhb.js → index-DWxg-UlU.js} +3 -3
  23. package/gui/dist/assets/{model-display-OBTJhQwT.js → model-display-vDl3mo07.js} +1 -1
  24. package/gui/dist/assets/{provider-payload-CJRJUn7w.js → provider-payload-C3Xp5N0y.js} +1 -1
  25. package/gui/dist/assets/{section-tabs-B8sLMiz6.js → section-tabs-Bpm5lFcZ.js} +1 -1
  26. package/gui/dist/assets/shared-BHpPx-nZ.js +69 -0
  27. package/gui/dist/index.html +3 -3
  28. package/package.json +1 -1
  29. package/src/adapters/base.ts +2 -0
  30. package/src/adapters/command-code.ts +2 -1
  31. package/src/adapters/google-http.ts +34 -14
  32. package/src/adapters/google.ts +4 -1
  33. package/src/cli/index.ts +9 -1
  34. package/src/codex/inject.ts +14 -9
  35. package/src/config/process-state.ts +15 -0
  36. package/src/config.ts +17 -0
  37. package/src/generated/compatibility-version.json +61 -41
  38. package/src/lib/bun-stream-caps.ts +2 -1
  39. package/src/lib/pinned-http.ts +7 -1
  40. package/src/lib/process-control.ts +3 -2
  41. package/src/lib/provider-outbound.ts +77 -27
  42. package/src/lib/provider-runtime-fetch.ts +23 -0
  43. package/src/lib/provider-tls-profile.ts +97 -11
  44. package/src/lib/server-tls.ts +47 -0
  45. package/src/lib/test-provider-fetch.ts +6 -0
  46. package/src/lib/test-server-start.ts +5 -0
  47. package/src/lib/upstream-retry.ts +39 -25
  48. package/src/oauth/anthropic.ts +4 -3
  49. package/src/oauth/chatgpt.ts +7 -3
  50. package/src/oauth/command-code.ts +3 -2
  51. package/src/oauth/cursor.ts +3 -2
  52. package/src/oauth/github-copilot.ts +7 -5
  53. package/src/oauth/google-antigravity.ts +5 -4
  54. package/src/oauth/key-providers.ts +11 -13
  55. package/src/oauth/kimi.ts +7 -6
  56. package/src/oauth/kiro.ts +3 -2
  57. package/src/oauth/nous.ts +5 -5
  58. package/src/oauth/transport.ts +77 -0
  59. package/src/oauth/xai.ts +5 -4
  60. package/src/providers/replit/probe.ts +8 -3
  61. package/src/providers/xai-transport.ts +16 -2
  62. package/src/server/auth-cors.ts +15 -1
  63. package/src/server/chat-native.ts +9 -3
  64. package/src/server/index.ts +63 -23
  65. package/src/server/management/config-routes.ts +117 -15
  66. package/src/server/management/context.ts +4 -0
  67. package/src/server/management/provider-routes.ts +1 -0
  68. package/src/server/responses/compact.ts +5 -2
  69. package/src/server/responses/core.ts +16 -19
  70. package/src/server/responses/fetch-helpers.ts +15 -7
  71. package/src/server/ws-bridge.ts +33 -4
  72. package/src/types/config.ts +9 -0
  73. package/src/types/provider.ts +2 -0
  74. package/src/types.ts +1 -0
  75. package/gui/dist/assets/Providers-DyaCHRWL.js +0 -1
  76. package/gui/dist/assets/index-pLho_LBF.css +0 -1
  77. package/gui/dist/assets/shared-DI18-uF7.js +0 -69
@@ -16,9 +16,9 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-DHQHBRhb.js"></script>
20
- <link rel="modulepreload" crossorigin href="/assets/shared-DI18-uF7.js">
21
- <link rel="stylesheet" crossorigin href="/assets/index-pLho_LBF.css">
19
+ <script type="module" crossorigin src="/assets/index-DWxg-UlU.js"></script>
20
+ <link rel="modulepreload" crossorigin href="/assets/shared-BHpPx-nZ.js">
21
+ <link rel="stylesheet" crossorigin href="/assets/index-C8flH881.css">
22
22
  </head>
23
23
  <body>
24
24
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yansigit/opencodex",
3
- "version": "2.36.1",
3
+ "version": "2.36.2-dev.20260829.52",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -143,6 +143,8 @@ export interface AdapterFetchContext {
143
143
  stream?: boolean;
144
144
  /** Custom fetch executor to use for physical upstream network requests (defaults to globalThis.fetch). */
145
145
  executor?: typeof globalThis.fetch;
146
+ /** Shared transient replay allowance for one logical generation. */
147
+ replayBudget?: { remaining: number };
146
148
  }
147
149
 
148
150
  /**
@@ -14,6 +14,7 @@ import { identifyRoutedModel } from "./identity";
14
14
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
15
15
  import { parseDataUrl } from "./image";
16
16
  import { redactSecretString } from "../lib/redact";
17
+ import { testProviderFetch } from "../lib/test-provider-fetch";
17
18
 
18
19
  // Retain the short ids emitted by the first local integration. New requests use the live catalog's
19
20
  // provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
@@ -600,7 +601,7 @@ function supportedCommandCodeEffort(provider: OcxProviderConfig, modelId: string
600
601
  }
601
602
 
602
603
  export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderAdapter {
603
- const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
604
+ const executor = testProviderFetch(provider) ?? globalThis.fetch;
604
605
  return {
605
606
  name: "command-code",
606
607
  formatErrorBody: formatCommandCodeErrorBody,
@@ -281,6 +281,8 @@ export interface GoogleRetryOptions {
281
281
  repairInvalid400?: boolean;
282
282
  /** Let the Responses layer own Antigravity's single same-account 429 replay. */
283
283
  retry429?: boolean;
284
+ replayTransientFailures?: boolean;
285
+ replayBudget?: { remaining: number };
284
286
  }
285
287
 
286
288
  async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
@@ -358,7 +360,13 @@ async function fetchGoogleWithRetryInternal(
358
360
  let antigravityHostIndex = 0;
359
361
  let lastError: unknown;
360
362
  let compatibilityReplayUsed = false;
361
- for (let attempt = 0; attempt < GOOGLE_RETRY_ATTEMPTS; attempt++) {
363
+ const maxAttempts = opts.replayTransientFailures ? GOOGLE_RETRY_ATTEMPTS : 1;
364
+ const consumeReplay = (): boolean => {
365
+ if (!opts.replayTransientFailures || (opts.replayBudget && opts.replayBudget.remaining <= 0)) return false;
366
+ if (opts.replayBudget) opts.replayBudget.remaining -= 1;
367
+ return true;
368
+ };
369
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
362
370
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
363
371
  try {
364
372
  const res = await fetchWithAttemptDeadline(activeRequest.url, {
@@ -374,7 +382,7 @@ async function fetchGoogleWithRetryInternal(
374
382
  }
375
383
  if (antigravityHosts.length > 1 && antigravityHostIndex === 0) {
376
384
  const shouldTryPeer = res.status === 404 || isUnavailableResponse(res);
377
- if (shouldTryPeer) {
385
+ if (shouldTryPeer && consumeReplay()) {
378
386
  cancelResponseBodyBestEffort(res);
379
387
  antigravityHostIndex = 1;
380
388
  activeRequest = requestForHost(activeRequest, antigravityHosts[antigravityHostIndex]!);
@@ -382,14 +390,16 @@ async function fetchGoogleWithRetryInternal(
382
390
  }
383
391
  }
384
392
  if (label === "Antigravity" && isAntigravitySseRequest(activeRequest) && res.ok) {
385
- const fetchPeer = antigravityHosts.length > 1 && antigravityHostIndex === 0
386
- ? () => fetchGoogleWithRetryInternal(
393
+ const fetchPeer = antigravityHosts.length > 1 && antigravityHostIndex === 0 && opts.replayTransientFailures
394
+ ? () => consumeReplay()
395
+ ? fetchGoogleWithRetryInternal(
387
396
  label,
388
397
  requestForHost(activeRequest, antigravityHosts[1]!),
389
398
  ctx,
390
399
  false,
391
400
  opts,
392
- )
401
+ )
402
+ : Promise.reject(new Error("Antigravity transient replay budget exhausted"))
393
403
  : undefined;
394
404
  return prepareCcaSseResponse(res, fetchPeer, ctx.accountId);
395
405
  }
@@ -422,7 +432,8 @@ async function fetchGoogleWithRetryInternal(
422
432
  if (res.status === 429 && !retry429) {
423
433
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
424
434
  }
425
- if (!retryableGoogleStatus(res.status) || attempt === GOOGLE_RETRY_ATTEMPTS - 1) {
435
+ if (!retryableGoogleStatus(res.status) || attempt === maxAttempts - 1
436
+ || (opts.replayBudget && opts.replayBudget.remaining <= 0)) {
426
437
  return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
427
438
  }
428
439
  // A 429 may be a transient rate limit (retry) or hard quota exhaustion (do NOT retry —
@@ -443,19 +454,21 @@ async function fetchGoogleWithRetryInternal(
443
454
  maxDelayMs: GOOGLE_RETRY_MAX_MS,
444
455
  headers: res.headers,
445
456
  }), ctx.abortSignal);
457
+ if (!consumeReplay()) return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
446
458
  } catch (err) {
447
459
  if (ctx.abortSignal?.aborted) throw err;
448
460
  lastError = err;
449
- if (antigravityHosts.length > 1 && antigravityHostIndex === 0) {
461
+ if (antigravityHosts.length > 1 && antigravityHostIndex === 0 && consumeReplay()) {
450
462
  antigravityHostIndex = 1;
451
463
  activeRequest = requestForHost(activeRequest, antigravityHosts[antigravityHostIndex]!);
452
464
  continue;
453
465
  }
454
- if (attempt === GOOGLE_RETRY_ATTEMPTS - 1) throw err;
466
+ if (attempt === maxAttempts - 1 || (opts.replayBudget && opts.replayBudget.remaining <= 0)) throw err;
455
467
  await sleepWithAbort(retryBackoffDelayMs(attempt, {
456
468
  baseDelayMs: GOOGLE_RETRY_BASE_MS,
457
469
  maxDelayMs: GOOGLE_RETRY_MAX_MS,
458
470
  }), ctx.abortSignal);
471
+ if (!consumeReplay()) throw err;
459
472
  }
460
473
  }
461
474
  throw lastError ?? new Error(`${label} fetch failed`);
@@ -480,16 +493,23 @@ export function fetchGoogleWithRetry(
480
493
  * demand" spikes, plus plain rate-limit 429s, both of which previously failed immediately
481
494
  * because the default server fetch path only retries connection resets.
482
495
  */
483
- export function fetchDirectGeminiWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
484
- return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, { repairInvalid400: false });
496
+ export function fetchDirectGeminiWithRetry(
497
+ request: AdapterRequest,
498
+ ctx: AdapterFetchContext = {},
499
+ opts: GoogleRetryOptions = {},
500
+ ): Promise<Response> {
501
+ return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, {
502
+ ...opts,
503
+ repairInvalid400: false,
504
+ });
485
505
  }
486
506
 
487
507
  /** Vertex AI retry wrapper. */
488
- export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
489
- return fetchGoogleWithRetry("Vertex AI", request, ctx);
508
+ export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}, opts: GoogleRetryOptions = {}): Promise<Response> {
509
+ return fetchGoogleWithRetry("Vertex AI", request, ctx, opts);
490
510
  }
491
511
 
492
512
  /** Antigravity (Cloud Code Assist) retry wrapper. */
493
- export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
494
- return fetchGoogleWithRetry("Antigravity", request, ctx, { retry429: false });
513
+ export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}, opts: GoogleRetryOptions = {}): Promise<Response> {
514
+ return fetchGoogleWithRetry("Antigravity", request, ctx, { ...opts, retry429: false });
495
515
  }
@@ -860,7 +860,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
860
860
  ...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
861
861
  ? {
862
862
  fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> =>
863
- (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx),
863
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(request, ctx, {
864
+ replayTransientFailures: provider.replayTransientFailures,
865
+ replayBudget: ctx?.replayBudget,
866
+ }),
864
867
  formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
865
868
  (provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
866
869
  }
package/src/cli/index.ts CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  } from "./tray-proxy";
39
39
  import { requestBoundSystemRestart } from "./system-restart-client";
40
40
  import { installCrashGuards } from "../lib/crash-guard";
41
+ import { canonicalServerOrigin } from "../lib/server-tls";
41
42
  import { dispatchCommand } from "./dispatch";
42
43
  import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
43
44
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
@@ -299,7 +300,14 @@ async function handleStart(options: { block?: boolean } = {}) {
299
300
  writePid(process.pid);
300
301
 
301
302
  const config = loadConfig();
302
- writeRuntimePort({ pid: process.pid, port, hostname: config.hostname, attestationSecret: localAttestationSecret });
303
+ const boundPort = server.port ?? port;
304
+ writeRuntimePort({
305
+ pid: process.pid,
306
+ port: boundPort,
307
+ hostname: config.hostname,
308
+ origin: canonicalServerOrigin(config, boundPort),
309
+ attestationSecret: localAttestationSecret,
310
+ });
303
311
  // No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
304
312
  // is about to transform; snapshotting earlier only captured a baseline that could
305
313
  // already be stale by the time injection ran (#477).
@@ -218,14 +218,16 @@ export function buildProviderTableBlock(
218
218
  supportsWebsockets = false,
219
219
  includeApiAuthHeader = false,
220
220
  hostname?: string,
221
+ publicOrigin?: string,
221
222
  ): string {
222
223
  const host = providerBaseHost(hostname);
224
+ const baseUrl = publicOrigin ?? `http://${host}:${port}`;
223
225
  const lines = [
224
226
  "",
225
227
  OCX_SECTION_MARKER,
226
228
  "[model_providers.opencodex]",
227
229
  'name = "OpenCodex Proxy"',
228
- `base_url = "http://${host}:${port}/v1"`,
230
+ `base_url = "${baseUrl}/v1"`,
229
231
  'wire_api = "responses"',
230
232
  "requires_openai_auth = true",
231
233
  ];
@@ -245,8 +247,9 @@ export function buildProviderTableBlock(
245
247
  export function buildOpenaiBaseUrlLine(
246
248
  port: number,
247
249
  hostname?: string,
250
+ publicOrigin?: string,
248
251
  ): string {
249
- return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
252
+ return `openai_base_url = "${publicOrigin ?? `http://${providerBaseHost(hostname)}:${port}`}/v1"`;
250
253
  }
251
254
 
252
255
  /**
@@ -259,11 +262,12 @@ export function setRootOpenaiBaseUrl(
259
262
  content: string,
260
263
  port: number,
261
264
  hostname?: string,
265
+ publicOrigin?: string,
262
266
  ): { content: string; keptUserBaseUrl: boolean } {
263
267
  const lines = content.split("\n");
264
268
  const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
265
269
  const rootEnd = firstTable === -1 ? lines.length : firstTable;
266
- const key = buildOpenaiBaseUrlLine(port, hostname);
270
+ const key = buildOpenaiBaseUrlLine(port, hostname, publicOrigin);
267
271
 
268
272
  for (let i = 0; i < rootEnd; i++) {
269
273
  if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
@@ -621,7 +625,7 @@ function stripOpencodexCatalogPath(content: string): string {
621
625
  .join("\n");
622
626
  }
623
627
 
624
- export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
628
+ export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean, publicOrigin?: string): string {
625
629
  const host = providerBaseHost(hostname);
626
630
  // Design B (loopback): the reference/fallback file documents the root override form.
627
631
  // Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
@@ -631,7 +635,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
631
635
  "# OpenCodex proxy fallback config (Design B)",
632
636
  `# Root override that points Codex's built-in openai provider at the proxy on ${host}:${port}.`,
633
637
  "# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
634
- buildOpenaiBaseUrlLine(port, hostname),
638
+ buildOpenaiBaseUrlLine(port, hostname, publicOrigin),
635
639
  ];
636
640
  if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
637
641
  if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
@@ -644,7 +648,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
644
648
  ];
645
649
  if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
646
650
  if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
647
- lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
651
+ lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname, publicOrigin).trimEnd(), "");
648
652
  return lines.join("\n");
649
653
  }
650
654
 
@@ -726,7 +730,7 @@ export async function injectCodexConfig(
726
730
  message:
727
731
  `⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
728
732
  ` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
729
- ` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
733
+ ` Configure that provider for Responses passthrough at ${config?.tls?.publicOrigin ?? `http://${providerBaseHost(config?.hostname)}:${port}`}/v1` +
730
734
  `${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
731
735
  ` For direct injection, switch to the built-in openai provider, remove any user-owned root openai_base_url, and rerun 'ocx start'.`,
732
736
  });
@@ -811,12 +815,13 @@ export async function injectCodexConfig(
811
815
  websocketsEnabled(config ?? {}),
812
816
  true,
813
817
  config?.hostname,
818
+ config?.tls?.publicOrigin,
814
819
  );
815
820
  } else {
816
821
  // Design B (loopback): a single root override; codex keeps its native `openai` provider id
817
822
  // so thread history is never remapped. Any legacy form was already stripped above.
818
823
  content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
819
- const result = setRootOpenaiBaseUrl(content, port, config?.hostname);
824
+ const result = setRootOpenaiBaseUrl(content, port, config?.hostname, config?.tls?.publicOrigin);
820
825
  content = result.content;
821
826
  keptUserBaseUrl = result.keptUserBaseUrl;
822
827
  }
@@ -852,7 +857,7 @@ export async function injectCodexConfig(
852
857
  managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
853
858
  }
854
859
 
855
- const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
860
+ const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode, config?.tls?.publicOrigin);
856
861
  content = applyEol(content, eol);
857
862
 
858
863
  /*
@@ -34,6 +34,8 @@ export type RuntimePortState = {
34
34
  pid: number;
35
35
  port: number;
36
36
  hostname?: string;
37
+ /** Canonical listener origin. New clients prefer this; legacy records omit it. */
38
+ origin?: string;
37
39
  /** Per-process proof key; protected by the config directory and never served. */
38
40
  attestationSecret?: string;
39
41
  };
@@ -42,6 +44,18 @@ function isValidRuntimePortState(value: unknown): value is RuntimePortState {
42
44
  if (!value || typeof value !== "object") return false;
43
45
  const state = value as Record<string, unknown>;
44
46
  const hostnameOk = state.hostname === undefined || typeof state.hostname === "string";
47
+ const originOk = state.origin === undefined || (() => {
48
+ if (typeof state.origin !== "string") return false;
49
+ try {
50
+ const url = new URL(state.origin);
51
+ return (url.protocol === "http:" || url.protocol === "https:")
52
+ && !url.username && !url.password
53
+ && url.pathname === "/" && !url.search && !url.hash
54
+ && url.origin === state.origin;
55
+ } catch {
56
+ return false;
57
+ }
58
+ })();
45
59
  const attestationOk = state.attestationSecret === undefined || isLocalAttestationSecret(state.attestationSecret);
46
60
  return Number.isSafeInteger(state.pid)
47
61
  && Number(state.pid) > 0
@@ -49,6 +63,7 @@ function isValidRuntimePortState(value: unknown): value is RuntimePortState {
49
63
  && Number(state.port) > 0
50
64
  && Number(state.port) <= 65535
51
65
  && hostnameOk
66
+ && originOk
52
67
  && attestationOk;
53
68
  }
54
69
 
package/src/config.ts CHANGED
@@ -150,6 +150,7 @@ export {
150
150
  writeRuntimePort,
151
151
  type RuntimePortState,
152
152
  } from "./config/process-state";
153
+ import { serverTlsConfigError } from "./lib/server-tls";
153
154
  import {
154
155
  clearPendingConfigTopLevelDeletions,
155
156
  configHasRebaseProvenance,
@@ -536,6 +537,7 @@ const providerConfigSchema = z.object({
536
537
  .transform(normalizeNonBlankStringArray)
537
538
  .optional(),
538
539
  retryOn429: retryOn429PolicySchema.optional(),
540
+ replayTransientFailures: z.boolean().optional(),
539
541
  codexAccountMode: z.enum(["pool", "direct"]).optional(),
540
542
  // Validated rather than passed through: this schema ends in `.passthrough()`, so an
541
543
  // undeclared key survives verbatim. A misspelled `codexToolMode` therefore used to be
@@ -904,6 +906,11 @@ const configSchema = z.object({
904
906
  // is safe: startServer() already falls back to 127.0.0.1 for a missing hostname. Write-time
905
907
  // rejection lives in validateConfigCandidate() so bad values still surface to the caller.
906
908
  hostname: z.string().trim().min(1).optional().catch(undefined),
909
+ tls: z.object({
910
+ certFile: z.string().trim().min(1),
911
+ keyFile: z.string().trim().min(1),
912
+ publicOrigin: z.string().trim().min(1),
913
+ }).strict().optional().catch(undefined),
907
914
  // Discriminated on `enabled` so a disabled entry cannot be forced to carry a port, and an
908
915
  // enabled one cannot omit it (#1102). A malformed value degrades to undefined rather than
909
916
  // failing the whole parse: this is an opt-in convenience surface, and a hand-edit typo here
@@ -1418,6 +1425,11 @@ function sanitizeRetryOn429ForLoad(parsed: unknown): void {
1418
1425
  const safeProviderName = JSON.stringify(redactSecretString(name));
1419
1426
  if (!provider || typeof provider !== "object" || Array.isArray(provider)) continue;
1420
1427
  const p = provider as Record<string, unknown>;
1428
+ if (p.replayTransientFailures !== undefined && typeof p.replayTransientFailures !== "boolean") {
1429
+ const receivedType = typeof p.replayTransientFailures;
1430
+ delete p.replayTransientFailures;
1431
+ console.warn(`⚠️ config.json providers.${safeProviderName}.replayTransientFailures (${receivedType}) is invalid — ignoring the field`);
1432
+ }
1421
1433
  const policy = p.retryOn429;
1422
1434
  if (policy === undefined) continue;
1423
1435
  if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
@@ -2385,6 +2397,11 @@ function loopbackListenerPortError(value: unknown): string | null {
2385
2397
 
2386
2398
  export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
2387
2399
  const boundaryError = blankHostnameError(value)
2400
+ ?? (() => {
2401
+ const raw = rawConfigRecord(value);
2402
+ const error = raw ? serverTlsConfigError(raw.tls) : null;
2403
+ return error ? `schema_invalid: ${error}` : null;
2404
+ })()
2388
2405
  ?? claudeSubagentEffortError(value)
2389
2406
  ?? subagentRolesError(value)
2390
2407
  ?? appOwnedMemoryBudgetError(value)