@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,529 @@
1
+ import type { OcxProviderConfig, OcxConfig } from "../../types";
2
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
3
+ import type { CodexAuthContext } from "../../codex/auth-context";
4
+ import type { RouteResult } from "../../router";
5
+ import type { HandleResponsesOptions } from "./core-options";
6
+ import {
7
+ hasForwardableCodexBearer,
8
+ validateForwardAdmissionCredential,
9
+ isProxyAdmissionSecret,
10
+ ForwardAdmissionCredentialError,
11
+ } from "../auth-cors";
12
+ import {
13
+ providerConsumesCallerAuthorization,
14
+ captureCallerDirectAuth,
15
+ } from "../../providers/caller-authorization";
16
+ import { inspectChatGptDomainClaim } from "../../oauth/chatgpt";
17
+ import {
18
+ resolveCodexAuthContext,
19
+ CodexMainProfileDrainingError,
20
+ materializeCodexUpstreamAuthAsync,
21
+ headersForCodexAuthContext,
22
+ isCodexAuthContextUsable,
23
+ releaseCodexAuthContextProbeLease,
24
+ CodexAuthContextError,
25
+ applyCodexAuthContextToProvider,
26
+ stripCodexRuntimeProviderFields,
27
+ } from "../../codex/auth-context";
28
+ import { codexAccountSelectionForTurn, tryClaimNativeMainProfileForTurn } from "../lifecycle";
29
+ import { isNativeMainTrafficBlocked } from "../../codex/native-profile-startup";
30
+ import { formatErrorResponse } from "../../bridge";
31
+ import { clientCancelledResponse } from "./core-errors";
32
+ import { formatCodexProviderForLog, handOffThreadAffinityGeneration } from "../../codex/routing";
33
+ import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
34
+ import {
35
+ isTerminalCodexPoolRefreshFailure,
36
+ forceRefreshCodexPoolToken,
37
+ capturePoolQuotaWriter,
38
+ } from "../../codex/account-store";
39
+ import type { RequestLogContext } from "../request-log";
40
+ import { markLocalRequestLogRefusal } from "../request-log";
41
+ import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
42
+ import { codexAuthContextLogLabel } from "../../codex/account-label";
43
+ import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
+
45
+ /** Keep synthesized Claude identity out of request headers reused by policy/combo fallback. */
46
+ export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
47
+ if (!sessionId || !isCanonicalOpenAiForwardProvider(provider)
48
+ || headers.has("session_id") || headers.has("session-id") || headers.has("thread-id")) return headers;
49
+ const forwarded = new Headers(headers);
50
+ forwarded.set("session_id", sessionId);
51
+ return forwarded;
52
+ }
53
+
54
+
55
+ export type ResponsesAuthResolution =
56
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers; callerAuthHeaders: Headers; substituteMainCredential: boolean }
57
+ | { ok: false; response: Response };
58
+
59
+
60
+ /**
61
+ * The caller credential the final Codex auth resolution will be given, as far as the ROUTE
62
+ * decides it: a route change that may cross a credential domain drops the raw caller credential,
63
+ * and a trusted Claude-main handoff replaces it.
64
+ *
65
+ * Shared with the lineage preview in `handleResponsesInner`, which has to read a conversation's
66
+ * family under the same authenticated scope the resolution will record it under -- that scope is
67
+ * an HMAC of exactly this Authorization header. Two copies of this rule would put preview and
68
+ * final auth in different scopes the first time one of them changed.
69
+ */
70
+ export function codexRouteCredentialDomainHeaders(
71
+ req: Request,
72
+ route: RouteResult,
73
+ options: HandleResponsesOptions,
74
+ credentialDomainWasRewritten: boolean,
75
+ ): Headers {
76
+ const trustedClaudeMainForFinalRoute = options.stripClaudeMainAuthForNoncanonicalForward === true
77
+ && isCanonicalOpenAiForwardProvider(route.provider)
78
+ ? options.trustedClaudeMainAuth : undefined;
79
+ if (trustedClaudeMainForFinalRoute) {
80
+ const claudeMainHeaders = new Headers(req.headers);
81
+ claudeMainHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
82
+ if (trustedClaudeMainForFinalRoute.chatgptAccountId) {
83
+ claudeMainHeaders.set("chatgpt-account-id", trustedClaudeMainForFinalRoute.chatgptAccountId);
84
+ } else {
85
+ claudeMainHeaders.delete("chatgpt-account-id");
86
+ }
87
+ return claudeMainHeaders;
88
+ }
89
+ // Route-changing recursion retains typed admission, never an unscoped raw
90
+ // caller credential. Bearer admission is substituted or stripped below.
91
+ const routeMayChangeCredentialDomain = options.comboAttempt === true
92
+ || route.routeKind === "policy"
93
+ || credentialDomainWasRewritten;
94
+ if (routeMayChangeCredentialDomain && options.admission?.source !== "bearer") {
95
+ const scoped = new Headers(req.headers);
96
+ scoped.delete("authorization");
97
+ scoped.delete("chatgpt-account-id");
98
+ return scoped;
99
+ }
100
+ return req.headers;
101
+ }
102
+
103
+
104
+ /**
105
+ * Does this route substitute OUR stored main credential, and does the caller own the credential
106
+ * this request will authenticate with?
107
+ *
108
+ * Both answers are needed twice: by the resolution below, and by the lineage preview, which must
109
+ * not follow a Pool family binding for a request whose credential never enters Pool state. One
110
+ * implementation, because two copies of this predicate disagreeing is the divergence the preview
111
+ * gate exists to prevent. The reasoning behind the substitution test itself is at its use site
112
+ * below (#1686, #2132).
113
+ */
114
+ export function codexRouteCredentialOwnership(
115
+ authInputHeaders: Headers,
116
+ config: OcxConfig,
117
+ route: RouteResult,
118
+ options: HandleResponsesOptions,
119
+ ): { substituteMainCredential: boolean; requestScopedMainCredential: boolean } {
120
+ const substituteMainCredential = options.admission?.source === "bearer"
121
+ && (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
122
+ return {
123
+ substituteMainCredential,
124
+ requestScopedMainCredential: route.codexAccountMode !== undefined
125
+ && !substituteMainCredential
126
+ && hasForwardableCodexBearer(authInputHeaders, config),
127
+ };
128
+ }
129
+
130
+
131
+ /**
132
+ * Resolve Codex auth for a route. On unusable contexts, releases any probe lease
133
+ * before returning the 401 (nothing reaches upstream).
134
+ */
135
+ export async function resolveResponsesCodexAuth(
136
+ req: Request,
137
+ config: OcxConfig,
138
+ route: RouteResult,
139
+ options: HandleResponsesOptions,
140
+ credentialDomainWasRewritten = false,
141
+ retainAccountForUploadedFiles = false,
142
+ ): Promise<ResponsesAuthResolution> {
143
+ try {
144
+ let authInputHeaders = codexRouteCredentialDomainHeaders(
145
+ req,
146
+ route,
147
+ options,
148
+ credentialDomainWasRewritten,
149
+ );
150
+ // A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
151
+ // caller's Authorization as its own upstream token. Keep that contract only for a clean
152
+ // single bearer with NO ChatGPT-domain marker. A bearer marked for the ChatGPT domain —
153
+ // whether its marker is valid or malformed/conflicting — a combined/malformed value, or
154
+ // the captured explicit OpenAI pair is never a Cursor token; a foreign JWT carrying only
155
+ // a generic organizations claim is not ChatGPT-marked and keeps the legacy contract.
156
+ // chatgpt-account-id has no meaning outside the ChatGPT domain.
157
+ if (!isCanonicalOpenAiForwardProvider(route.provider)
158
+ && providerConsumesCallerAuthorization(route.provider)) {
159
+ const rawAuth = authInputHeaders.get("authorization")?.trim();
160
+ const singleBearer = /^Bearer[\t ]+([^\s,]+)$/i.exec(rawAuth ?? "")?.[1];
161
+ const domainClaim = singleBearer ? inspectChatGptDomainClaim(singleBearer) : { kind: "absent" as const };
162
+ const dropBearer = options.nativeCallerAuth != null || domainClaim.kind !== "absent"
163
+ || (rawAuth !== undefined && singleBearer === undefined);
164
+ if (dropBearer || authInputHeaders.has("chatgpt-account-id")) {
165
+ const scoped = new Headers(authInputHeaders);
166
+ if (dropBearer) scoped.delete("authorization");
167
+ scoped.delete("chatgpt-account-id");
168
+ authInputHeaders = scoped;
169
+ }
170
+ }
171
+ // The caller's own Direct credential may cross an internal route change only to the
172
+ // canonical OpenAI transport, under a predicate deliberately STRICTER than plain
173
+ // unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
174
+ // marker is valid, with any explicit account header matching that marker. Unchanged
175
+ // routes keep their legacy rules; sidecar enrichment grants no primary authority.
176
+ if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
177
+ const directHeaders = new Headers({
178
+ authorization: options.callerDirectAuth.authorization,
179
+ ...(options.callerDirectAuth.chatgptAccountId
180
+ ? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
181
+ });
182
+ if (captureCallerDirectAuth(directHeaders, config)) {
183
+ authInputHeaders = new Headers(authInputHeaders);
184
+ authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
185
+ if (options.callerDirectAuth.chatgptAccountId) {
186
+ authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
187
+ } else {
188
+ authInputHeaders.delete("chatgpt-account-id");
189
+ }
190
+ }
191
+ }
192
+ // #1686: a caller that proved admission with a BEARER presented one of our own secrets.
193
+ // Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
194
+ // Admitting it is only safe because the stored main credential is substituted below, so
195
+ // the admission secret still never leaves this process.
196
+ //
197
+ // #2132: substitution answers "does THIS ROUTE need our stored ChatGPT credential", not
198
+ // "how did the caller authenticate". Only a native Codex route reaches the ChatGPT backend
199
+ // and can consume that credential; a key-authenticated routed provider carries its own and
200
+ // never touches it. Keying on the caller alone made an install that deliberately never
201
+ // logged into ChatGPT fail every routed request with "No usable Codex main credential".
202
+ //
203
+ // But ask that question the way the ADAPTER asks it. `codexAccountMode` is derived from the
204
+ // provider NAME (`providerCodexAccountMode`), while the passthrough adapter decides whether
205
+ // to forward caller credentials from the TRANSPORT — adapter, auth mode, and base URL
206
+ // (`isCanonicalOpenAiForwardProvider`). A row the operator named anything other than
207
+ // `openai`, pointed at the canonical ChatGPT backend with `authMode: "forward"`, satisfies
208
+ // the adapter's test and fails this one, so substitution was skipped and the adapter then
209
+ // forwarded our own admission secret upstream. Two predicates answering one question is the
210
+ // bug; the transport is the authority, because the transport is what actually carries the
211
+ // header. A key-authenticated routed provider is still not canonical-forward, so #2132's
212
+ // no-ChatGPT-login install keeps working.
213
+ const { substituteMainCredential, requestScopedMainCredential } = codexRouteCredentialOwnership(
214
+ authInputHeaders,
215
+ config,
216
+ route,
217
+ options,
218
+ );
219
+ const stripAuthorization = options.admission?.source === "bearer" && !substituteMainCredential;
220
+ if (route.codexAccountMode === "direct" && !substituteMainCredential) {
221
+ validateForwardAdmissionCredential(authInputHeaders, config);
222
+ }
223
+ let authCtx: CodexAuthContext;
224
+ if (route.codexAccountMode) {
225
+ authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
226
+ admission: options.admission,
227
+ codexAuthPolicy: options.codexAuthPolicy,
228
+ accountId: route.codexAccountId,
229
+ modelId: route.modelId,
230
+ substituteMainCredentialForDirect: substituteMainCredential,
231
+ requestScopedMainCredential,
232
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
233
+ resolveCodexModelEntitlements: options.resolveCodexModelEntitlements,
234
+ signal: options.abortSignal,
235
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
236
+ retainAccountForUploadedFiles,
237
+ });
238
+ options.onCodexAuthContextResolved?.(authCtx);
239
+ } else {
240
+ // A custom-named canonical-forward provider has no Codex account mode, but an
241
+ // admission bearer still substitutes the stored main credential below. Claim the
242
+ // same physical profile before synthesizing the main context so transport-based
243
+ // substitution cannot bypass a switch drain.
244
+ if (
245
+ substituteMainCredential
246
+ && (
247
+ isNativeMainTrafficBlocked()
248
+ || !tryClaimNativeMainProfileForTurn(options.turnAdmissionLease)
249
+ || isNativeMainTrafficBlocked()
250
+ )
251
+ ) {
252
+ throw new CodexMainProfileDrainingError();
253
+ }
254
+ authCtx = { kind: "main", accountId: null };
255
+ options.onCodexAuthContextResolved?.(undefined);
256
+ }
257
+ // This resolver also builds a synthetic main context for unrelated keyed routes. Only
258
+ // the actual Codex-forward transport consumes main quota; provider names are not proof
259
+ // (custom-named canonical-forward providers must retain the same protection).
260
+ const mainPolicyConfig = isCanonicalOpenAiForwardProvider(route.provider)
261
+ ? options.codexAuthPolicy ?? config : undefined;
262
+ const headers = await materializeCodexUpstreamAuthAsync(authInputHeaders, authCtx, {
263
+ admission: options.admission,
264
+ config: mainPolicyConfig,
265
+ modelId: route.modelId,
266
+ beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
267
+ substituteMainCredential,
268
+ signal: options.abortSignal,
269
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
270
+ });
271
+ // Awaiting even a cached materialization yields. Preserve the policy error if the live
272
+ // quota/config changed during that yield, before usability could mislabel it as reauth.
273
+ headersForCodexAuthContext(headers, authCtx, mainPolicyConfig, route.modelId, options.admission);
274
+ if (!isCodexAuthContextUsable(authCtx, config)) {
275
+ releaseCodexAuthContextProbeLease(authCtx);
276
+ return {
277
+ ok: false,
278
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
279
+ };
280
+ }
281
+ if (stripAuthorization) {
282
+ headers.delete("authorization");
283
+ headers.delete("chatgpt-account-id");
284
+ }
285
+ if (providerConsumesCallerAuthorization(route.provider) && options.admission?.source !== undefined
286
+ && options.admission.source !== "loopback") {
287
+ validateForwardAdmissionCredential(headers, config);
288
+ } else {
289
+ // Even adapters that ignore caller auth must not retain a proxy secret for
290
+ // a later internal hop or a future transport change.
291
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
292
+ if (bearer && isProxyAdmissionSecret(bearer, config)) {
293
+ headers.delete("authorization");
294
+ headers.delete("chatgpt-account-id");
295
+ }
296
+ }
297
+ return {
298
+ ok: true,
299
+ authCtx,
300
+ headers,
301
+ callerAuthHeaders: new Headers(authInputHeaders),
302
+ substituteMainCredential,
303
+ };
304
+ } catch (err) {
305
+ if (options.abortSignal?.aborted || req.signal.aborted) {
306
+ return { ok: false, response: clientCancelledResponse() };
307
+ }
308
+ if (err instanceof CodexAuthContextError) {
309
+ const safeAccountLabel = route.codexAccountNamespace
310
+ ? `${route.providerName}-${route.codexAccountNamespace}`
311
+ : formatCodexProviderForLog(route.providerName, err.accountId, config);
312
+ console.error(`[codex-auth] Pool account ${safeAccountLabel} token failed; reauthentication required`);
313
+ }
314
+ if (err instanceof ForwardAdmissionCredentialError) {
315
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
316
+ }
317
+ const response = mapCodexAuthContextErrorToResponse(err, {
318
+ accountSelector: route.codexAccountNamespace,
319
+ now: Date.now(),
320
+ });
321
+ if (response) return { ok: false, response };
322
+ throw err;
323
+ }
324
+ }
325
+
326
+
327
+ /**
328
+ * Terminal means the grant itself is dead and no retry can help. Everything else —
329
+ * an untyped network failure, a token-endpoint 5xx surfacing as `unknown`, an abort,
330
+ * refresh capacity, lock contention, a superseded flight — is transient, and treating
331
+ * it as terminal would quarantine a healthy account on an upstream blip, which is the
332
+ * defect this path exists to fix (#2887).
333
+ */
334
+ export function isTerminalPoolRefreshFailure(error: unknown): boolean {
335
+ // Delegated so "terminal" has ONE definition. A missing record or a missing refresh-grant
336
+ // fingerprint is permanent -- retrying cannot conjure a credential -- and used to be a bare
337
+ // Error, which fell through to the retryable 503 and told the operator to keep retrying a
338
+ // request that could never succeed.
339
+ return isTerminalCodexPoolRefreshFailure(error);
340
+ }
341
+
342
+
343
+ /**
344
+ * The refusal an operator meets when a stored pool credential's forced refresh does not complete.
345
+ *
346
+ * A bare "retry this request" reads as a transient fault in the proxy, which is how #4212's
347
+ * reporter spent an afternoon concluding OpenCodex had broken while one of their own accounts was
348
+ * the thing that needed them. It stays a retryable 503 and stays non-quarantining, because the
349
+ * refresh genuinely may succeed and a token-endpoint 5xx must not retire a healthy account
350
+ * (#2887). What it adds is the account and the exit: when retrying stops helping, that account
351
+ * has to be signed in again.
352
+ *
353
+ * The label is a public account selector when the request carried one, otherwise the durable
354
+ * `p`-prefixed log label — never the raw pool id and never the email. Those are the identifiers
355
+ * `responses-compaction-routing.test.ts` and `codex-auth-context.test.ts` already assert must not
356
+ * reach an operator-facing surface, and an error body travels further than a log line, not less.
357
+ * When neither is resolvable the sentence degrades to "the selected Codex pool account" rather
358
+ * than naming something opaque, because a wrong name is worse than no name.
359
+ *
360
+ * The wording says "sign in to that account again" and deliberately does NOT say
361
+ * "reauthentication". `classifyError` runs `isAuthenticationMessage` before it reaches the
362
+ * `status === 503` arm, and that check is status-blind on the bare substring "authentication",
363
+ * which "reauthentication" contains. A body carrying that word is reclassified to
364
+ * `authentication_error` / `invalid_api_key` even though the HTTP status stays 503 — and Codex
365
+ * applies retry-after backoff only for `server_is_overloaded`, so the friendlier sentence would
366
+ * have quietly disabled the retry this refusal exists to ask for. `options.code` cannot buy the
367
+ * classification back; only the wording can.
368
+ */
369
+ export function poolCredentialRefreshIncompleteResponse(args: {
370
+ authCtx: CodexAuthContext;
371
+ config: Pick<OcxConfig, "codexAccounts">;
372
+ accountSelector?: string;
373
+ logCtx?: RequestLogContext;
374
+ }): Response {
375
+ // The wire contract below is unchanged on purpose, so the record has to carry the origin
376
+ // instead. Without it an operator reads this sentence under a field named "Upstream reason"
377
+ // and goes looking at the provider's status page for a refusal that never left this process.
378
+ if (args.logCtx) markLocalRequestLogRefusal(args.logCtx, CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON);
379
+ const label = args.accountSelector ?? codexAuthContextLogLabel(args.authCtx, args.config);
380
+ const account = label ? `Codex pool account ${label}` : "the selected Codex pool account";
381
+ const response = formatErrorResponse(
382
+ 503,
383
+ "server_busy",
384
+ `Codex credential refresh did not complete for ${account}; retry this request. `
385
+ + "If it keeps failing, sign in to that account again.",
386
+ );
387
+ const headers = new Headers(response.headers);
388
+ headers.set("Retry-After", "1");
389
+ return new Response(response.body, { status: response.status, headers });
390
+ }
391
+
392
+
393
+ /**
394
+ * One forced refresh and one same-account rebuild for a stored pool credential that
395
+ * upstream rejected with a pre-stream 401. `quarantine` distinguishes a dead grant,
396
+ * which must retire the account, from a transient failure, which must not.
397
+ */
398
+ export async function refreshPoolForwardAuth(args: {
399
+ logCtx?: RequestLogContext;
400
+ req: Request;
401
+ config: OcxConfig;
402
+ route: RouteResult;
403
+ authCtx: CodexAuthContext & { kind: "pool" };
404
+ substituteMainCredential: boolean;
405
+ options: HandleResponsesOptions;
406
+ }): Promise<
407
+ | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
408
+ | { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
409
+ > {
410
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
411
+ try {
412
+ const refreshed = await forceRefreshCodexPoolToken(authCtx.accountId, {
413
+ rejectedGeneration: authCtx.generation,
414
+ rejectedAccessToken: authCtx.accessToken,
415
+ signal: options.abortSignal,
416
+ });
417
+ if (!refreshed.rotated) {
418
+ // The store resolved to the same bearer upstream just rejected. Replaying it
419
+ // would spend another upstream call to earn the identical 401. Upstream can do
420
+ // this on a SUCCESSFUL response by rotating only the refresh grant, so the
421
+ // credential generation may already have moved — quarantine has to be fenced on
422
+ // where the credential actually is, not on the generation we started from.
423
+ return {
424
+ ok: false,
425
+ quarantine: true,
426
+ quarantineGeneration: refreshed.generation,
427
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
428
+ };
429
+ }
430
+ // Only a CAS this request performed itself proves the new credential descends from
431
+ // the rejected one. Somebody else's replacement may be a different identity, and
432
+ // its affinity must be retired rather than inherited.
433
+ if (refreshed.selfRefreshed) {
434
+ handOffThreadAffinityGeneration(authCtx.accountId, authCtx.generation, refreshed.generation);
435
+ }
436
+ const refreshedAuthCtx: CodexAuthContext = {
437
+ ...authCtx,
438
+ accessToken: refreshed.accessToken,
439
+ chatgptAccountId: refreshed.chatgptAccountId,
440
+ generation: refreshed.generation,
441
+ poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
442
+ };
443
+ const provider = applyCodexAuthContextToProvider(
444
+ stripCodexRuntimeProviderFields(route.provider),
445
+ refreshedAuthCtx,
446
+ route.codexAccountMode,
447
+ );
448
+ const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
449
+ admission: options.admission,
450
+ config: options.codexAuthPolicy ?? config,
451
+ modelId: route.modelId,
452
+ substituteMainCredential,
453
+ signal: options.abortSignal,
454
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
455
+ });
456
+ return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
457
+ } catch (error) {
458
+ if (isTerminalPoolRefreshFailure(error)) {
459
+ return {
460
+ ok: false,
461
+ quarantine: true,
462
+ response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
463
+ };
464
+ }
465
+ return {
466
+ ok: false,
467
+ quarantine: false,
468
+ response: poolCredentialRefreshIncompleteResponse({
469
+ authCtx,
470
+ config,
471
+ accountSelector: route.codexAccountNamespace,
472
+ logCtx: args.logCtx,
473
+ }),
474
+ };
475
+ }
476
+ }
477
+
478
+
479
+ export async function refreshNativeMainForwardAuth(args: {
480
+ req: Request;
481
+ config: OcxConfig;
482
+ route: RouteResult;
483
+ authCtx: CodexAuthContext;
484
+ substituteMainCredential: boolean;
485
+ options: HandleResponsesOptions;
486
+ }): Promise<
487
+ | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
488
+ | { ok: false; response: Response }
489
+ > {
490
+ const { req, config, route, authCtx, substituteMainCredential, options } = args;
491
+ if (authCtx.kind !== "main-pool") {
492
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
493
+ }
494
+ try {
495
+ const refreshed = await forceRefreshMainAccountToken(authCtx.accessToken, {
496
+ signal: options.abortSignal,
497
+ ...(options.nativeMainRefreshDependencies ?? {}),
498
+ });
499
+ if (!refreshed) {
500
+ return { ok: false, response: formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication") };
501
+ }
502
+ const refreshedAuthCtx: CodexAuthContext = {
503
+ ...authCtx,
504
+ accessToken: refreshed.accessToken,
505
+ chatgptAccountId: refreshed.chatgptAccountId,
506
+ };
507
+ const provider = applyCodexAuthContextToProvider(
508
+ stripCodexRuntimeProviderFields(route.provider),
509
+ refreshedAuthCtx,
510
+ route.codexAccountMode,
511
+ );
512
+ const headers = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
513
+ admission: options.admission,
514
+ config: options.codexAuthPolicy ?? config,
515
+ modelId: route.modelId,
516
+ substituteMainCredential,
517
+ signal: options.abortSignal,
518
+ nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
519
+ });
520
+ return { ok: true, authCtx: refreshedAuthCtx, provider, headers };
521
+ } catch (error) {
522
+ if (options.abortSignal?.aborted || req.signal.aborted) {
523
+ return { ok: false, response: clientCancelledResponse() };
524
+ }
525
+ return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
526
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
527
+ }) ?? nativeMainRefreshFailureResponse(error) };
528
+ }
529
+ }