@bitkyc08/opencodex 2.78.0 → 2.79.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 (71) hide show
  1. package/gui/dist/assets/App-BOUa-ek3.js +54 -0
  2. package/gui/dist/assets/App-CSJvmjNs.css +1 -0
  3. package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-DUtzB4zF.js} +1 -1
  4. package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
  5. package/gui/dist/assets/index-DwHNDF4s.js +94 -0
  6. package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-BuL4WIBo.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +3 -2
  9. package/src/adapters/devin.ts +22 -1
  10. package/src/claude/desktop-picker.ts +11 -9
  11. package/src/claude/inbound-content-options.ts +29 -2
  12. package/src/claude/intercept/local-ca-files.ts +265 -0
  13. package/src/claude/intercept/local-ca-windows.ts +73 -0
  14. package/src/claude/intercept/local-ca.ts +12 -24
  15. package/src/claude/intercept/picker-ca-persistence.ts +106 -0
  16. package/src/claude/intercept/picker-ca-startup.ts +16 -0
  17. package/src/claude/intercept/picker-ca-store.ts +70 -0
  18. package/src/claude/intercept/picker-ca.ts +43 -9
  19. package/src/claude/intercept/picker-runtime.ts +14 -4
  20. package/src/claude/intercept/runtime.ts +32 -23
  21. package/src/cli/ensure-readiness.ts +48 -0
  22. package/src/cli/gui-pair-client.ts +15 -7
  23. package/src/cli/gui.ts +4 -1
  24. package/src/cli/hub.ts +1 -0
  25. package/src/cli/index.ts +10 -11
  26. package/src/cli/status.ts +2 -2
  27. package/src/cli/update-restart-child.ts +4 -1
  28. package/src/codex/account-credit-use.ts +8 -2
  29. package/src/codex/auth-api/account-pause-group.ts +113 -0
  30. package/src/codex/auth-api/routes.ts +18 -12
  31. package/src/codex/auth-context.ts +45 -3
  32. package/src/codex/autostart-health.ts +10 -0
  33. package/src/codex/pool-credit-policy.ts +67 -0
  34. package/src/generated/compatibility-version.json +106 -46
  35. package/src/lib/gui-pair-capability.ts +1 -1
  36. package/src/lib/gui-pair-intent.ts +170 -0
  37. package/src/lib/windows-owner-acl.ts +143 -0
  38. package/src/providers/openai-sidecar-credit.ts +26 -0
  39. package/src/providers/openai-sidecar.ts +35 -0
  40. package/src/server/claude-messages.ts +1 -0
  41. package/src/server/gui-pair-delivery.ts +23 -0
  42. package/src/server/index/serve-options.ts +5 -8
  43. package/src/server/live-native-calls.ts +94 -0
  44. package/src/server/live.ts +28 -5
  45. package/src/server/local-management-read-client.ts +15 -11
  46. package/src/server/management/oauth-account-routes.ts +15 -0
  47. package/src/server/management/route-registry.ts +1 -0
  48. package/src/server/management/shared.ts +9 -3
  49. package/src/server/messages-native-scope.ts +57 -0
  50. package/src/server/messages-native.ts +9 -0
  51. package/src/server/responses/adapter-dispatch.ts +31 -2
  52. package/src/server/responses/compact.ts +17 -2
  53. package/src/server/responses/core-auth.ts +18 -1
  54. package/src/server/responses/core-codex-account.ts +18 -8
  55. package/src/server/responses/non-replayable-error.ts +48 -0
  56. package/src/server/responses/passthrough-delivery.ts +5 -1
  57. package/src/server/responses/passthrough-dispatch.ts +21 -19
  58. package/src/server/responses/request-prepare.ts +11 -1
  59. package/src/server/responses/sidecar-execution.ts +1 -0
  60. package/src/server/search.ts +4 -0
  61. package/src/server/startup-health-cache.ts +2 -6
  62. package/src/vision/describe.ts +17 -10
  63. package/src/vision/index.ts +1 -0
  64. package/src/web-search/executor.ts +17 -10
  65. package/src/web-search/format-result.ts +304 -20
  66. package/src/web-search/loop.ts +2 -1
  67. package/src/web-search/passthrough-bridge.ts +1 -0
  68. package/src/web-search/run-turn-loop.ts +1 -1
  69. package/gui/dist/assets/App-BtfG5id-.js +0 -54
  70. package/gui/dist/assets/App-lCIslZ1L.css +0 -1
  71. package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
@@ -110,6 +110,8 @@ import { linkAbortSignal } from "./responses/core-lifetime";
110
110
  import { attachRequestSpendTracker } from "./responses/request-spend";
111
111
  import { workflowRefusalResponse } from "./workflow-refusal";
112
112
  import { sseFieldValue } from "../lib/sse-decoder";
113
+ import { admissionModelDeniedResponse, type AdmissionModelScope } from "./admission-model-scope";
114
+ import { nativeMessagesToolScopeDenial } from "./messages-native-scope";
113
115
 
114
116
  export {
115
117
  isNativeMessagesRouteEligible,
@@ -159,6 +161,7 @@ export interface HandleNativeMessagesOptions {
159
161
  callerAnthropicBeta?: string | null;
160
162
  clientIdentity?: AnthropicClientIdentity;
161
163
  sessionKey?: string | null;
164
+ modelScope?: AdmissionModelScope;
162
165
  }
163
166
 
164
167
  type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
@@ -350,6 +353,12 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
350
353
  return null;
351
354
  };
352
355
 
356
+ const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
357
+ if (toolScopeDenial) {
358
+ finishLog(403, toolScopeDenial.message);
359
+ return admissionModelDeniedResponse(toolScopeDenial);
360
+ }
361
+
353
362
  try {
354
363
  await prepareNativeBody(body, req.signal);
355
364
  } catch (error) {
@@ -4,6 +4,7 @@ import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
4
4
  import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
5
5
  import { transientSendCapFor } from "./request-send-budget";
6
6
  import { isNonReplayableResponse } from "../../lib/upstream-retry";
7
+ import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
7
8
  import { isLocalUpstream } from "../../lib/local-upstream";
8
9
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
9
10
  import type { PreparedResponsesRequest } from "./request-prepare";
@@ -100,6 +101,9 @@ import {
100
101
  } from "../../lib/errors";
101
102
  import { resolveClientRetryAfter } from "../../lib/retry-after";
102
103
  import { cancelBodyOnAbort } from "../../lib/abort";
104
+ import { createCodexAuthDispatchGuard, releaseCodexAuthContextProbeLease, unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
105
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
106
+ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
103
107
  import { chargeWorkflowSends } from "../../lib/workflow-budget";
104
108
  import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
105
109
 
@@ -344,6 +348,8 @@ export async function prepareAdapterExchange(
344
348
  dispatchOverride: oauthDispatch(builtInitialRequest),
345
349
  providerName: route.providerName,
346
350
  modelId: route.modelId,
351
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
352
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
347
353
  }),
348
354
  }));
349
355
  } else {
@@ -384,6 +390,8 @@ export async function prepareAdapterExchange(
384
390
  dispatchOverride: oauthDispatch(builtInitialRequest),
385
391
  providerName: route.providerName,
386
392
  modelId: route.modelId,
393
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
394
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
387
395
  }));
388
396
  },
389
397
  {
@@ -406,6 +414,13 @@ export async function prepareAdapterExchange(
406
414
  upstream.abort();
407
415
  if (options.abortSignal?.aborted) return clientCancelledResponse();
408
416
  const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
417
+ const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
418
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
419
+ });
420
+ if (codexRefusal) {
421
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
422
+ return codexRefusal;
423
+ }
409
424
  // A pause committed during pacing is local admission policy, not a failed upstream.
410
425
  if (refusal instanceof OAuthAccountPausedError) {
411
426
  return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
@@ -550,6 +565,8 @@ export async function prepareAdapterExchange(
550
565
  dispatchOverride: oauthDispatch(retryRequest),
551
566
  providerName: route.providerName,
552
567
  modelId: route.modelId,
568
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
569
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
553
570
  }),
554
571
  });
555
572
  });
@@ -601,6 +618,8 @@ export async function prepareAdapterExchange(
601
618
  dispatchOverride: oauthDispatch(retryRequest),
602
619
  providerName: route.providerName,
603
620
  modelId: route.modelId,
621
+ beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
622
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
604
623
  }));
605
624
  },
606
625
  {
@@ -624,6 +643,15 @@ export async function prepareAdapterExchange(
624
643
  retryRequest.releaseBodyObservation?.();
625
644
  }
626
645
  } catch (err) {
646
+ const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
647
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
648
+ });
649
+ if (codexRefusal) {
650
+ cleanupUpstreamAbort();
651
+ upstream.abort();
652
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
653
+ return { failed: codexRefusal };
654
+ }
627
655
  if (preserveFailureResponse && !replacementAdmitted && !options.abortSignal?.aborted)
628
656
  return { failed: preserveFailureResponse };
629
657
  cleanupUpstreamAbort();
@@ -704,8 +732,9 @@ export async function prepareAdapterExchange(
704
732
  // Preserve the terminal verdict through adapter and combo error formatting.
705
733
  // This also covers a reset reached by a 401/429/413 recovery refetch.
706
734
  if (isNonReplayableResponse(upstreamResponse)) {
707
- cleanupUpstreamAbort();
708
- return upstreamResponse;
735
+ try {
736
+ return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
737
+ } finally { cleanupUpstreamAbort(); }
709
738
  }
710
739
  if (
711
740
  upstreamResponse.status === 401
@@ -53,7 +53,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
53
53
  import {
54
54
  applyCodexAuthContextToProvider,
55
55
  callerCodexWorkspaceAccountId,
56
- createCodexReserveDispatchGuard,
56
+ createCodexAuthDispatchGuard,
57
57
  unwrapUpstreamRetryEvidenceError,
58
58
  CodexMainProfileDrainingError,
59
59
  headersForCodexAuthContext,
@@ -182,6 +182,8 @@ import {
182
182
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
183
183
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
184
184
  import { codexAuthContextLogLabel } from "../../codex/account-label";
185
+ import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
186
+ import { clientCancelledResponse } from "./core-errors";
185
187
 
186
188
  import {
187
189
  codexAccountGatedCanonicalWireModel,
@@ -435,6 +437,9 @@ async function refreshPoolCompactContext(args: {
435
437
  generation: refreshed.generation,
436
438
  poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
437
439
  };
440
+ // See the core counterpart: the spread dropped the WeakMap-bound credit policy, so a hold
441
+ // or opt-out landing during the refresh await would go unchecked on the copy.
442
+ rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
438
443
  const refreshedProvider = applyCodexAuthContextToProvider(
439
444
  stripCodexRuntimeProviderFields(provider),
440
445
  refreshedAuthCtx,
@@ -463,6 +468,16 @@ async function refreshPoolCompactContext(args: {
463
468
  if (isTerminalCompactPoolRefreshFailure(error)) {
464
469
  return { ok: false, quarantine: true, response: reauthResponse() };
465
470
  }
471
+ if (req.signal.aborted) {
472
+ return { ok: false, quarantine: false, response: clientCancelledResponse() };
473
+ }
474
+ // See the core counterpart: a policy refusal raised after a successful refresh maps like
475
+ // the admission path — a reset-bound 429, never a 503 mislabeled as a refresh failure.
476
+ const policyResponse = mapCodexAuthContextErrorToResponse(error, {
477
+ now: Date.now(),
478
+ accountSelector: args.codexAccountNamespace,
479
+ });
480
+ if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
466
481
  return {
467
482
  ok: false,
468
483
  quarantine: false,
@@ -1042,7 +1057,7 @@ export async function handleResponsesCompact(
1042
1057
  providerName: route.providerName,
1043
1058
  modelId: route.modelId,
1044
1059
  beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
1045
- ? createCodexReserveDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
1060
+ ? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
1046
1061
  }),
1047
1062
  // Every credential-bearing forward send gets manual redirects, not only
1048
1063
  // pool sends: direct mode carries the caller's credential too (#914).
@@ -41,6 +41,7 @@ import { markLocalRequestLogRefusal } from "../request-log";
41
41
  import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
42
42
  import { codexAuthContextLogLabel } from "../../codex/account-label";
43
43
  import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
+ import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
44
45
  import { safeCallerSessionId } from "../caller-session-identity";
45
46
 
46
47
  /** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
@@ -200,6 +201,7 @@ export async function resolveResponsesCodexAuth(
200
201
  credentialDomainWasRewritten = false,
201
202
  retainAccountForUploadedFiles = false,
202
203
  ): Promise<ResponsesAuthResolution> {
204
+ let authCtx: CodexAuthContext | undefined;
203
205
  try {
204
206
  let authInputHeaders = codexRouteCredentialDomainHeaders(
205
207
  req,
@@ -260,7 +262,6 @@ export async function resolveResponsesCodexAuth(
260
262
  if (route.codexAccountMode === "direct" && !substituteMainCredential) {
261
263
  validateForwardAdmissionCredential(authInputHeaders, config);
262
264
  }
263
- let authCtx: CodexAuthContext;
264
265
  if (route.codexAccountMode) {
265
266
  authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
266
267
  admission: options.admission,
@@ -342,6 +343,7 @@ export async function resolveResponsesCodexAuth(
342
343
  substituteMainCredential,
343
344
  };
344
345
  } catch (err) {
346
+ releaseCodexAuthContextProbeLease(authCtx);
345
347
  if (options.abortSignal?.aborted || req.signal.aborted) {
346
348
  return { ok: false, response: clientCancelledResponse() };
347
349
  }
@@ -480,6 +482,9 @@ export async function refreshPoolForwardAuth(args: {
480
482
  generation: refreshed.generation,
481
483
  poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
482
484
  };
485
+ // The spread dropped the WeakMap-bound credit policy: without the live binding a hold or
486
+ // opt-out landing during the refresh await would go unchecked on every no-override path.
487
+ rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
483
488
  const provider = applyCodexAuthContextToProvider(
484
489
  stripCodexRuntimeProviderFields(route.provider),
485
490
  refreshedAuthCtx,
@@ -502,6 +507,18 @@ export async function refreshPoolForwardAuth(args: {
502
507
  response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
503
508
  };
504
509
  }
510
+ if (options.abortSignal?.aborted || req.signal.aborted) {
511
+ return { ok: false, quarantine: false, response: clientCancelledResponse() };
512
+ }
513
+ // The credential itself refreshed; a policy refusal raised afterwards (a credit hold or an
514
+ // opt-out that landed during the await) is not a refresh failure. Map it the same way the
515
+ // admission path does — a reset-bound 429, never a 503 telling the caller to sign in — and
516
+ // never quarantine a valid account for a policy refusal.
517
+ const policyResponse = mapCodexAuthContextErrorToResponse(error, {
518
+ accountSelector: route.codexAccountNamespace,
519
+ now: Date.now(),
520
+ });
521
+ if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
505
522
  return {
506
523
  ok: false,
507
524
  quarantine: false,
@@ -54,7 +54,8 @@ import {
54
54
  headersForCodexAuthContext,
55
55
  applyCodexAuthContextToProvider,
56
56
  stripCodexRuntimeProviderFields,
57
- createCodexReserveDispatchGuard,
57
+ createCodexAuthDispatchGuard,
58
+ CodexPoolAccountCreditsOffError,
58
59
  } from "../../codex/auth-context";
59
60
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
60
61
  import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
@@ -745,12 +746,21 @@ export async function retryCodexPoolOnAlternateAccount(
745
746
  // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
746
747
  // ordinary requests must block the first account before the alternate send.
747
748
  if (!deferFirstOutcome) recordFirstOutcome();
748
- const retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
749
- const retryProvider = applyCodexAuthContextToProvider(
750
- stripCodexRuntimeProviderFields(route.provider),
751
- retryAuthCtx,
752
- "pool",
753
- );
749
+ let retryHeaders: Headers;
750
+ let retryProvider: ReturnType<typeof applyCodexAuthContextToProvider>;
751
+ try {
752
+ retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
753
+ retryProvider = applyCodexAuthContextToProvider(stripCodexRuntimeProviderFields(route.provider), retryAuthCtx, "pool");
754
+ } catch (error) {
755
+ if (!(error instanceof CodexPoolAccountCreditsOffError)) throw error;
756
+ // Body/entitlement reads above can outlive credit consent. No alternate will send:
757
+ // release its reservation and probes, then let the owner map the policy refusal.
758
+ accountMovePermit?.release();
759
+ releaseCodexAuthContextProbeLease(firstAuthCtx);
760
+ releaseCodexAuthContextProbeLease(retryAuthCtx);
761
+ await firstResponse.body?.cancel().catch(() => undefined);
762
+ return { kind: "transport", error, authCtx: retryAuthCtx };
763
+ }
754
764
  const retryAdapter = resolveAdapter(
755
765
  resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
756
766
  config.cacheRetention,
@@ -889,7 +899,7 @@ export async function retryCodexPoolOnAlternateAccount(
889
899
  modelId: route.modelId,
890
900
  onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
891
901
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
892
- ? createCodexReserveDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
902
+ ? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
893
903
  }),
894
904
  // Credential-bearing forward send: never follow a redirect into a
895
905
  // dead-host rejection after the credential was seen (#914).
@@ -0,0 +1,48 @@
1
+ import { isNonReplayableResponse, isReplayRefusalResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
2
+ import { clientCancelledResponse, readDisplaySafeErrorText } from "./core-errors";
3
+
4
+ const UPSTREAM_ERROR_TYPES = new Set([
5
+ "invalid_request_error", "authentication_error", "permission_error", "not_found_error",
6
+ "rate_limit_error", "request_too_large", "overloaded_error", "api_error", "server_error", "insufficient_quota",
7
+ ]);
8
+ const UPSTREAM_ERROR_CODES = new Set([
9
+ "context_length_exceeded", "rate_limit_exceeded", "insufficient_quota", "invalid_api_key",
10
+ "model_not_found", "invalid_prompt", "content_policy_violation", "server_error",
11
+ "unsupported_parameter", "invalid_value", "string_above_max_length",
12
+ ]);
13
+
14
+ /** Client projection never grants another send or turns a real upstream error into our refusal. */
15
+ export async function sanitizeNonReplayableUpstreamError(
16
+ response: Response,
17
+ signal: AbortSignal,
18
+ ): Promise<Response> {
19
+ if (response.ok || !isNonReplayableResponse(response) || isReplayRefusalResponse(response)) return response;
20
+ const text = await readDisplaySafeErrorText(response, signal, "");
21
+ if (signal.aborted) {
22
+ const cancelled = clientCancelledResponse();
23
+ markResponseNonReplayable(cancelled);
24
+ return cancelled;
25
+ }
26
+ let type = "upstream_error";
27
+ let code: string | undefined;
28
+ try {
29
+ const parsed = JSON.parse(text);
30
+ const upstreamType = parsed?.error?.type;
31
+ const upstreamCode = parsed?.error?.code;
32
+ if (typeof upstreamType === "string" && UPSTREAM_ERROR_TYPES.has(upstreamType)) type = upstreamType;
33
+ if (typeof upstreamCode === "string" && UPSTREAM_ERROR_CODES.has(upstreamCode)) code = upstreamCode;
34
+ } catch {
35
+ // Incomplete or non-JSON bodies retain the generic error type.
36
+ }
37
+ // Upstream text is not forwarded here: no bounded redaction can be proven complete against a client's decoders.
38
+ const body = JSON.stringify({ error: { type, ...(code === undefined ? {} : { code }),
39
+ message: `Provider error ${response.status}: upstream diagnostic withheld for a non-replayable failure`,
40
+ } });
41
+ const headers = new Headers({ "content-type": "application/json" });
42
+ if (response.headers.get("x-should-retry") === "false") headers.set("x-should-retry", "false");
43
+ const safe = new Response(response.status === 304 ? null : body, {
44
+ status: response.status, headers,
45
+ });
46
+ markResponseNonReplayable(safe);
47
+ return safe;
48
+ }
@@ -18,7 +18,8 @@ import {
18
18
  relayWithAbort,
19
19
  } from "../relay";
20
20
  import { isUsageDebugEnabled } from "../../usage/debug";
21
- import { isReplayRefusalResponse } from "../../lib/upstream-retry";
21
+ import { isNonReplayableResponse, isReplayRefusalResponse } from "../../lib/upstream-retry";
22
+ import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
22
23
  import { teeWithBoundedInspection } from "../inspection-tee";
23
24
  import {
24
25
  codexForwardTerminalOutcomeRecorder,
@@ -543,6 +544,9 @@ export async function deliverPassthroughResponse(
543
544
  // through sanitizePassthroughHeaders) so a redirect to a dead host can never
544
545
  // masquerade as a pre-connection failure after the credential was seen.
545
546
  // The numeric outcome above already classified it neutral — no streak.
547
+ if (isNonReplayableResponse(upstreamResponse) && !isReplayRefusalResponse(upstreamResponse)) {
548
+ return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
549
+ }
546
550
  if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
547
551
  return new Response(upstreamResponse.body, {
548
552
  status: upstreamResponse.status,
@@ -37,7 +37,7 @@ import {
37
37
  codexProbeLeaseId,
38
38
  codexProbeQuotaScope,
39
39
  codexTransientProbeGrant,
40
- createCodexReserveDispatchGuard,
40
+ createCodexAuthDispatchGuard,
41
41
  } from "../../codex/auth-context";
42
42
  import {
43
43
  NamespaceToolCollisionError,
@@ -87,7 +87,6 @@ import {
87
87
  fetchWithHeaderTimeout,
88
88
  providerFetch,
89
89
  safeHostLabel,
90
- storedPoolReplayDispatchNotifier,
91
90
  } from "./fetch-helpers";
92
91
  import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
93
92
  import { clientCancelledResponse } from "./core-errors";
@@ -816,7 +815,7 @@ export async function preparePassthroughExchange(
816
815
  throw new Error("Credential selection changed before pre-output stream recovery");
817
816
  }
818
817
  if (isCanonicalOpenAiForwardProvider(route.provider)) {
819
- createCodexReserveDispatchGuard(
818
+ createCodexAuthDispatchGuard(
820
819
  admissionState.authCtx,
821
820
  options.codexAuthPolicy ?? config,
822
821
  route.modelId,
@@ -984,7 +983,7 @@ export async function preparePassthroughExchange(
984
983
  modelId: route.modelId,
985
984
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
986
985
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
987
- ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
986
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
988
987
  }),
989
988
  route.provider.authMode === "forward")
990
989
  // Every real attempt response — including an intermediate 5xx the
@@ -1089,7 +1088,7 @@ export async function preparePassthroughExchange(
1089
1088
  modelId: route.modelId,
1090
1089
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1091
1090
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1092
- ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1091
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1093
1092
  }),
1094
1093
  route.provider.authMode === "forward")
1095
1094
  .then(adoptObservedResponse);
@@ -1180,13 +1179,13 @@ export async function preparePassthroughExchange(
1180
1179
  // The replay-dispatched signal is what bounds the rest of this logical request, so it
1181
1180
  // has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
1182
1181
  // admission BEFORE calling the executor, so signalling at the call site would spend the
1183
- // budget even when a rejected pacing wait means nothing reaches the network. Wrapping
1184
- // the executor moves the signal to the last moment before the send, where a throw from
1185
- // here on is a genuine transport attempt. The notifier is built once for the whole leg:
1186
- // it fires on the first dispatch, and a replacement is another send of the same replay
1187
- // rather than a second one to announce.
1188
- const oauthReplayExecutor = storedPoolReplayDispatchNotifier(
1189
- providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1182
+ // budget even when a rejected pacing wait means nothing reaches the network. Signal
1183
+ // only after the physical-dispatch policy check succeeds; a local refusal is no send.
1184
+ // A replacement is another send of the same replay, rather than a second announcement.
1185
+ const replayGuard = isCanonicalOpenAiForwardProvider(route.provider)
1186
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined;
1187
+ let replayNotified = false;
1188
+ const oauthReplayExecutor = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1190
1189
  httpOnly: canonicalBufferedJson,
1191
1190
  nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
1192
1191
  && responseEffects.plaintextV2AgentMessageToolNames.size === 0
@@ -1195,11 +1194,14 @@ export async function preparePassthroughExchange(
1195
1194
  providerName: route.providerName,
1196
1195
  modelId: route.modelId,
1197
1196
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1198
- beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1199
- ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1200
- }),
1201
- codex401ReplayKind === "stored" ? options.onStoredPool401ReplayDispatched : undefined,
1202
- );
1197
+ beforeDispatch: headers => {
1198
+ replayGuard?.(headers);
1199
+ if (codex401ReplayKind === "stored" && !replayNotified) {
1200
+ replayNotified = true;
1201
+ options.onStoredPool401ReplayDispatched?.();
1202
+ }
1203
+ },
1204
+ });
1203
1205
  // Routed through the shared helper so an ambiguous reset on THIS leg answers with the
1204
1206
  // same refusal every other leg gives. A bare fetch here rejected instead, and the
1205
1207
  // caller's transport-failure path turns a rejection into a client-retryable 502 --
@@ -1346,7 +1348,7 @@ export async function preparePassthroughExchange(
1346
1348
  modelId: route.modelId,
1347
1349
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1348
1350
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1349
- ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1351
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1350
1352
  }),
1351
1353
  route.provider.authMode === "forward")
1352
1354
  .then(adoptObservedResponse);
@@ -1481,7 +1483,7 @@ export async function preparePassthroughExchange(
1481
1483
  modelId: route.modelId,
1482
1484
  onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
1483
1485
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1484
- ? createCodexReserveDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1486
+ ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1485
1487
  }),
1486
1488
  route.provider.authMode === "forward")
1487
1489
  .then(adoptObservedResponse);
@@ -46,6 +46,7 @@ import {
46
46
  codexPoolAffinityKey,
47
47
  previewCodexPoolLineage,
48
48
  applyCodexAuthContextToProvider,
49
+ releaseCodexAuthContextProbeLease,
49
50
  hasCallerCodexBearer,
50
51
  requestOwnedMainPinState,
51
52
  requestOwnedMainCredentialIsLive,
@@ -58,6 +59,7 @@ import {
58
59
  previousResponseProviderState,
59
60
  } from "../../responses/state";
60
61
  import { formatErrorResponse } from "../../bridge";
62
+ import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
61
63
  import type { OcxParsedRequest } from "../../types";
62
64
  import { buildToolBridgeMaps } from "./collaboration";
63
65
  import { parseRequest } from "../../responses/parser";
@@ -1352,7 +1354,15 @@ export async function prepareResponsesRequest(
1352
1354
  substituteMainCredential = finalAuth.substituteMainCredential;
1353
1355
  }
1354
1356
 
1355
- route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
1357
+ try {
1358
+ route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
1359
+ } catch (error) {
1360
+ releaseCodexAuthContextProbeLease(admissionState.authCtx);
1361
+ if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
1362
+ const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now(), accountSelector: route.codexAccountNamespace });
1363
+ if (mapped) return mapped;
1364
+ throw error;
1365
+ }
1356
1366
  applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
1357
1367
  try {
1358
1368
  recordPolicyPreparedDestination(policyScope, route.providerName, parsed._wireModelOverride ?? parsed.modelId);
@@ -510,6 +510,7 @@ export async function executeResponsesSidecars(
510
510
  transportState.bindKeyUsageFromBridge(usage);
511
511
  },
512
512
  recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
513
+ beforeSidecarDispatch: wsPlan.forwardSidecar?.beforeDispatch,
513
514
  connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
514
515
  routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
515
516
  stallTimeoutSec: wsPlan.stallTimeoutSec,
@@ -11,6 +11,7 @@
11
11
  * paid backend than the one the operator named.
12
12
  */
13
13
  import { formatErrorResponse } from "../bridge";
14
+ import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
14
15
  import {
15
16
  CodexAccountCooldownError,
16
17
  codexMainProfileDrainingResponse,
@@ -202,6 +203,7 @@ export async function handleSearch(
202
203
  const sidecarExit = sidecarEnter("search");
203
204
  let upstreamResponse: Response | undefined;
204
205
  try {
206
+ upstream.beforeDispatch?.();
205
207
  upstreamResponse = await fetch(url, {
206
208
  method: "POST",
207
209
  headers,
@@ -233,6 +235,8 @@ export async function handleSearch(
233
235
  if (req.signal.aborted) {
234
236
  return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
235
237
  }
238
+ const policyRefusal = openAiSidecarCreditRefusal(err);
239
+ if (policyRefusal) return cooldownErrorResponse(policyRefusal, Date.now(), accountNamespace);
236
240
  if (linkedSignal.signal.aborted || (err instanceof Error && err.name === "TimeoutError")) {
237
241
  upstream.recordOutcome?.("timeout");
238
242
  return formatErrorResponse(504, "upstream_error", "search upstream timed out");
@@ -1,7 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
  import { codexAutoStartEnabled } from "../config";
4
- import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health";
4
+ import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
5
5
  import { getCodexRoutingKind } from "../codex/inject";
6
6
  import { diagnoseCodexShim } from "../codex/shim";
7
7
  import { durableBunPath } from "../lib/bun-runtime";
@@ -11,7 +11,7 @@ import type { OcxConfig } from "../types";
11
11
  import { truncateRetainedUtf8 } from "../lib/admission";
12
12
 
13
13
  const CACHE_TTL_MS = 30_000;
14
- const PROBE_TIMEOUT_MS = probeTimeoutMs();
14
+ const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
15
15
  const INITIAL_PROBE_WAIT_MS = PROBE_TIMEOUT_MS + 500;
16
16
  const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
17
17
 
@@ -31,10 +31,6 @@ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
31
31
  * bound in both cases: a wedged probe is still abandoned, and the caller still
32
32
  * receives the previous reading rather than waiting on it.
33
33
  */
34
- function probeTimeoutMs(): number {
35
- return process.platform === "win32" ? 15_000 : 5_000;
36
- }
37
-
38
34
  /** The probe bound, so a test's own budget cannot fall below what it must wait for. */
39
35
  export function startupHealthProbeTimeoutMs(): number {
40
36
  return PROBE_TIMEOUT_MS;
@@ -8,6 +8,7 @@ import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-
8
8
  import { parseSidecarSSE } from "../web-search/parse";
9
9
  import type { SidecarOutcomeRecorder } from "../web-search/executor";
10
10
  import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
11
+ import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
11
12
 
12
13
  export interface VisionSettings {
13
14
  model: string;
@@ -60,6 +61,7 @@ export async function describeImage(
60
61
  settings: VisionSettings,
61
62
  abortSignal?: AbortSignal,
62
63
  recordOutcome?: SidecarOutcomeRecorder,
64
+ beforeDispatch?: () => void,
63
65
  ): Promise<DescribeOutcome> {
64
66
  if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
65
67
  return { text: "", error: "Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision helper model." };
@@ -98,16 +100,19 @@ export async function describeImage(
98
100
  const res = await fetchWithResetRetry(
99
101
  // The replay needs `keepalive: false` to abandon the half-closed pooled socket; Bun has
100
102
  // ignored a bare `Connection: close` (oven-sh/bun#20492).
101
- recovery => fetch(`${forwardProvider.baseUrl}/responses`, applyUpstreamRecoveryInit({
102
- method: "POST",
103
- headers,
104
- body: JSON.stringify(body),
105
- signal: linkedSignal.signal,
106
- // Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
107
- // across origins but forwards nonstandard headers such as `chatgpt-account-id`,
108
- // `session_id`, and `x-codex-turn-metadata` to the redirect target.
109
- redirect: "manual",
110
- }, recovery)),
103
+ recovery => {
104
+ beforeDispatch?.();
105
+ return fetch(`${forwardProvider.baseUrl}/responses`, applyUpstreamRecoveryInit({
106
+ method: "POST",
107
+ headers,
108
+ body: JSON.stringify(body),
109
+ signal: linkedSignal.signal,
110
+ // Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
111
+ // across origins but forwards nonstandard headers such as `chatgpt-account-id`,
112
+ // `session_id`, and `x-codex-turn-metadata` to the redirect target.
113
+ redirect: "manual",
114
+ }, recovery));
115
+ },
111
116
  { replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar" },
112
117
  );
113
118
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
@@ -129,6 +134,8 @@ export async function describeImage(
129
134
  detachBodyGuard();
130
135
  }
131
136
  } catch (e) {
137
+ const policyRefusal = openAiSidecarCreditRefusal(e);
138
+ if (policyRefusal) return { text: "", error: policyRefusal.message };
132
139
  const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
133
140
  const callerAborted = abortSignal?.aborted === true
134
141
  && linkedSignal.signal.aborted
@@ -269,6 +269,7 @@ async function executeDescription(
269
269
  plan.settings,
270
270
  abortSignal,
271
271
  recordSidecarOutcome,
272
+ plan.forwardSidecar.beforeDispatch,
272
273
  );
273
274
  }
274
275