@bitkyc08/opencodex 2.78.0 → 2.80.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 (91) hide show
  1. package/gui/dist/assets/App-CSJvmjNs.css +1 -0
  2. package/gui/dist/assets/App-MbDVfUeC.js +54 -0
  3. package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-CzkxK82A.js} +1 -1
  4. package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
  5. package/gui/dist/assets/index-UJt6npXk.js +94 -0
  6. package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-DU3sD6Z-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +3 -2
  9. package/src/adapters/cursor/live-transport.ts +5 -3
  10. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  11. package/src/adapters/cursor/native-exec.ts +13 -5
  12. package/src/adapters/cursor/protobuf-request.ts +13 -7
  13. package/src/adapters/cursor/request-builder.ts +23 -10
  14. package/src/adapters/cursor/tool-definitions.ts +9 -6
  15. package/src/adapters/cursor/tool-guidance.ts +29 -8
  16. package/src/adapters/cursor/tool-naming.ts +40 -7
  17. package/src/adapters/cursor/tool-wording.ts +46 -0
  18. package/src/adapters/cursor.ts +3 -0
  19. package/src/adapters/devin.ts +37 -2
  20. package/src/adapters/openai-chat/messages.ts +3 -1
  21. package/src/claude/desktop-picker.ts +11 -9
  22. package/src/claude/inbound-content-options.ts +29 -2
  23. package/src/claude/inbound.ts +9 -0
  24. package/src/claude/intercept/local-ca-files.ts +265 -0
  25. package/src/claude/intercept/local-ca-windows.ts +73 -0
  26. package/src/claude/intercept/local-ca.ts +12 -24
  27. package/src/claude/intercept/picker-ca-persistence.ts +106 -0
  28. package/src/claude/intercept/picker-ca-startup.ts +16 -0
  29. package/src/claude/intercept/picker-ca-store.ts +70 -0
  30. package/src/claude/intercept/picker-ca.ts +43 -9
  31. package/src/claude/intercept/picker-runtime.ts +14 -4
  32. package/src/claude/intercept/runtime.ts +32 -23
  33. package/src/cli/ensure-readiness.ts +48 -0
  34. package/src/cli/gui-pair-client.ts +15 -7
  35. package/src/cli/gui.ts +4 -1
  36. package/src/cli/hub.ts +1 -0
  37. package/src/cli/index.ts +10 -11
  38. package/src/cli/status.ts +2 -2
  39. package/src/cli/update-restart-child.ts +4 -1
  40. package/src/codex/account-credit-use.ts +8 -2
  41. package/src/codex/auth-api/account-pause-group.ts +113 -0
  42. package/src/codex/auth-api/routes.ts +18 -12
  43. package/src/codex/auth-context.ts +45 -3
  44. package/src/codex/autostart-health.ts +13 -3
  45. package/src/codex/journal.ts +36 -5
  46. package/src/codex/pool-credit-policy.ts +67 -0
  47. package/src/codex/shim-overlay.ts +9 -1
  48. package/src/combos/jev.ts +3 -1
  49. package/src/generated/compatibility-version.json +135 -63
  50. package/src/lib/gui-pair-capability.ts +1 -1
  51. package/src/lib/gui-pair-intent.ts +170 -0
  52. package/src/lib/windows-owner-acl.ts +143 -0
  53. package/src/providers/openai-sidecar-credit.ts +26 -0
  54. package/src/providers/openai-sidecar.ts +35 -0
  55. package/src/server/claude-messages.ts +1 -0
  56. package/src/server/gui-pair-delivery.ts +23 -0
  57. package/src/server/index/serve-options.ts +5 -8
  58. package/src/server/index.ts +12 -14
  59. package/src/server/live-native-calls.ts +94 -0
  60. package/src/server/live.ts +28 -5
  61. package/src/server/local-management-read-client.ts +15 -11
  62. package/src/server/management/oauth-account-routes.ts +15 -0
  63. package/src/server/management/route-registry.ts +1 -0
  64. package/src/server/management/shared.ts +9 -3
  65. package/src/server/messages-native-scope.ts +60 -0
  66. package/src/server/messages-native.ts +9 -0
  67. package/src/server/responses/adapter-dispatch.ts +31 -2
  68. package/src/server/responses/combo-requested-effort.ts +14 -0
  69. package/src/server/responses/compact.ts +17 -2
  70. package/src/server/responses/core-auth.ts +18 -1
  71. package/src/server/responses/core-codex-account.ts +18 -8
  72. package/src/server/responses/core-combo.ts +5 -7
  73. package/src/server/responses/non-replayable-error.ts +48 -0
  74. package/src/server/responses/passthrough-delivery.ts +5 -1
  75. package/src/server/responses/passthrough-dispatch.ts +21 -19
  76. package/src/server/responses/request-prepare.ts +11 -1
  77. package/src/server/responses/sidecar-execution.ts +1 -0
  78. package/src/server/search.ts +4 -0
  79. package/src/server/startup-health-cache.ts +2 -6
  80. package/src/service/desktop-startup.ts +100 -3
  81. package/src/vision/describe.ts +17 -10
  82. package/src/vision/index.ts +1 -0
  83. package/src/web-search/executor.ts +17 -10
  84. package/src/web-search/format-result.ts +304 -20
  85. package/src/web-search/header-deadline.ts +67 -0
  86. package/src/web-search/loop.ts +7 -6
  87. package/src/web-search/passthrough-bridge.ts +1 -0
  88. package/src/web-search/run-turn-loop.ts +1 -1
  89. package/gui/dist/assets/App-BtfG5id-.js +0 -54
  90. package/gui/dist/assets/App-lCIslZ1L.css +0 -1
  91. package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
@@ -44,9 +44,14 @@ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
44
44
  import { sidecarEnter } from "../lib/sidecar-tracker";
45
45
  import { hardenSecretPath } from "../lib/windows-secret-acl";
46
46
  import type { OcxConfig } from "../types";
47
- import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
47
+ import {
48
+ resolveCallerOwnedOpenAiSidecar,
49
+ resolveFirstUsableOpenAiSidecar,
50
+ selectOpenAiImagesProvider,
51
+ } from "../providers/openai-sidecar";
48
52
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "./auth-cors";
49
53
  import { admissionScopeDenial } from "./admission-model-scope";
54
+ import { nativeLiveCalls } from "./live-native-calls";
50
55
  import type { RequestLogContext } from "./request-log";
51
56
  import { codexLogAccountId } from "./responses";
52
57
  import type { AdmissionLease } from "../lib/admission";
@@ -613,6 +618,12 @@ async function readRequestBodyCapped(req: Request, maxBytes: number): Promise<Ar
613
618
  export interface LiveScopeDestination {
614
619
  admission?: DataPlaneAdmission;
615
620
  model: string | undefined;
621
+ /**
622
+ * The call a sideband join attaches to. When this process did not create it (no entry in
623
+ * `nativeLiveCalls`), the client created it with its own ChatGPT login — a V3 `existingCall` —
624
+ * and the join is authenticated as that caller instead of a Pool-selected account.
625
+ */
626
+ callerOwnedCallId?: string;
616
627
  }
617
628
 
618
629
  /**
@@ -691,9 +702,14 @@ export async function resolveLiveRelay(
691
702
  let forwardAuthError: Response | undefined;
692
703
  if (candidates.forwardCandidates.length > 0) {
693
704
  try {
694
- forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
695
- beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
696
- });
705
+ const joinsForeignCall = destination?.callerOwnedCallId !== undefined
706
+ && !nativeLiveCalls.has(destination.callerOwnedCallId);
707
+ forward = (joinsForeignCall
708
+ ? resolveCallerOwnedOpenAiSidecar(candidates.forwardCandidates, req.headers, config)
709
+ : undefined)
710
+ ?? await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
711
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
712
+ });
697
713
  if (forward) {
698
714
  logCtx.provider = formatCodexProviderForLog(
699
715
  forward.providerName,
@@ -756,7 +772,8 @@ export async function resolveLiveRelay(
756
772
  providerBaseUrl: provider.baseUrl,
757
773
  usesBackendShape: isChatGptBackendBaseUrl(provider.baseUrl),
758
774
  keyed: false,
759
- recordOutcome: status => forward.recordOutcome?.(status),
775
+ // A caller-owned or Direct credential selected no Pool account, so there is nothing to record.
776
+ ...(forward.recordOutcome ? { recordOutcome: (status: number | "timeout" | "connect_error") => forward.recordOutcome?.(status) } : {}),
760
777
  };
761
778
  }
762
779
  if (forwardAuthError) return forwardAuthError;
@@ -864,6 +881,11 @@ export async function handleLive(
864
881
  detachBodyGuard();
865
882
  }
866
883
  if (payload instanceof Response) return payload;
884
+ // Remember the calls this proxy created, so their joins keep the Pool account that created
885
+ // them while a join for any other call is authenticated as its caller (`resolveLiveRelay`).
886
+ if (upstreamResponse.status >= 200 && upstreamResponse.status < 300) {
887
+ nativeLiveCalls.record(upstreamResponse.headers.get("location"));
888
+ }
867
889
  const relayHeaders: Record<string, string> = {};
868
890
  for (const name of LIVE_RELAY_HEADERS) {
869
891
  const value = upstreamResponse.headers.get(name);
@@ -902,6 +924,7 @@ export async function resolveLiveSidebandUpgrade(
902
924
  const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
903
925
  admission,
904
926
  model: liveSidebandModel(target),
927
+ ...("callId" in target ? { callerOwnedCallId: target.callId } : {}),
905
928
  });
906
929
  if (relay instanceof Response) return relay;
907
930
  return {
@@ -25,6 +25,8 @@ export type LocalManagementReadResult =
25
25
 
26
26
  export interface LocalManagementReadDeps {
27
27
  fetchImpl?: typeof fetch;
28
+ /** Default transport seam; receives the caller deadline. */
29
+ directFetch?: typeof directLocalHttpFetch;
28
30
  readRuntime?: (pid: number) => RuntimePortState | null;
29
31
  createNonce?: () => string;
30
32
  now?: () => number;
@@ -84,18 +86,20 @@ export async function fetchBoundLocalManagementRead(
84
86
  if (!capability) return { kind: "unavailable", reason: "capability-unavailable" };
85
87
 
86
88
  try {
87
- const response = await (deps.fetchImpl ?? directLocalHttpFetch)(
88
- `http://${probeHostname(target.hostname)}:${target.port}${path}`,
89
- {
90
- headers: {
91
- [LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
92
- [LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
93
- [LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER]: String(expiresAt),
94
- [LOCAL_MANAGEMENT_CAPABILITY_HEADER]: capability,
95
- },
96
- signal: AbortSignal.timeout(deps.timeoutMs ?? 4_000),
89
+ const timeoutMs = deps.timeoutMs ?? 4_000;
90
+ const url = `http://${probeHostname(target.hostname)}:${target.port}${path}`;
91
+ const init: RequestInit = {
92
+ headers: {
93
+ [LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
94
+ [LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
95
+ [LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER]: String(expiresAt),
96
+ [LOCAL_MANAGEMENT_CAPABILITY_HEADER]: capability,
97
97
  },
98
- );
98
+ signal: AbortSignal.timeout(timeoutMs),
99
+ };
100
+ const response = deps.fetchImpl
101
+ ? await deps.fetchImpl(url, init)
102
+ : await (deps.directFetch ?? directLocalHttpFetch)(url, init, { timeoutMs });
99
103
  if (deps.requireResponseProof && !verifyLocalAttestationProof(
100
104
  runtime.attestationSecret, nonce, target.pid, target.port, response.headers.get(LOCAL_ATTESTATION_PROOF_HEADER),
101
105
  )) {
@@ -1164,6 +1164,21 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
1164
1164
  }, 200, req, config);
1165
1165
  }
1166
1166
 
1167
+ if (url.pathname === "/api/keys/reveal" && req.method === "POST") {
1168
+ if (principal !== "gui-session") {
1169
+ return jsonResponse({ error: "dashboard session required" }, 403, req, config);
1170
+ }
1171
+ const body = await readJsonBody(req);
1172
+ if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
1173
+ return jsonResponse({ error: "invalid body" }, 400, req, config);
1174
+ }
1175
+ const entry = config.apiKeys?.find(key => key.id === body.id);
1176
+ if (!entry) return jsonResponse({ error: "key not found" }, 404, req, config);
1177
+ const response = jsonResponse({ key: entry.key }, 200, req, config);
1178
+ response.headers.set("Cache-Control", "no-store");
1179
+ return response;
1180
+ }
1181
+
1167
1182
  if (url.pathname === "/api/keys/rotate" && req.method === "POST") {
1168
1183
  const body = await readJsonBody(req);
1169
1184
  if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
@@ -324,6 +324,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
324
324
  { method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
325
325
  { method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
326
326
  { method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
327
+ { method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Revealing a data key requires the gui-session principal; the admin token is refused." } },
327
328
  { method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
328
329
  { method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
329
330
  { method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
@@ -89,6 +89,11 @@ export type TokPerSecondResult =
89
89
  | { kind: "value"; value: number; estimated: boolean }
90
90
  | { kind: "unavailable"; reason: MetricUnavailableReason };
91
91
 
92
+ export type DecodeTimingBasis = "generation-window" | "legacy-post-visible-output";
93
+ export type DecodeTokPerSecondResult =
94
+ | (Extract<TokPerSecondResult, { kind: "value" }> & { timingBasis: DecodeTimingBasis })
95
+ | Extract<TokPerSecondResult, { kind: "unavailable" }>;
96
+
92
97
  export type CostEstimateReason =
93
98
  | "usage_estimated"
94
99
  | "cache_detail_missing"
@@ -149,7 +154,7 @@ export const MIN_DECODE_WINDOW_MS = 1_000;
149
154
  */
150
155
  export function decodeTokPerSecondResult(
151
156
  entry: Pick<MetricSource, "durationMs" | "firstOutputMs" | "genStartMs" | "lastOutputMs" | "usageStatus" | "usage">,
152
- ): TokPerSecondResult {
157
+ ): DecodeTokPerSecondResult {
153
158
  if (!entry.usage) return { kind: "unavailable", reason: "usage_missing" };
154
159
  if (entry.usageStatus === "unsupported") return { kind: "unavailable", reason: "usage_unsupported" };
155
160
  if (entry.usage.outputTokens <= 0) return { kind: "unavailable", reason: "output_missing" };
@@ -159,7 +164,8 @@ export function decodeTokPerSecondResult(
159
164
  if (generationMs <= 0) return { kind: "unavailable", reason: "invalid_duration" };
160
165
  if (generationMs < MIN_DECODE_WINDOW_MS) return { kind: "unavailable", reason: "decode_window_too_short" };
161
166
  const value = tokensPerSecond(entry.usage.outputTokens, generationMs);
162
- return value === null ? { kind: "unavailable", reason: "invalid_duration" } : { kind: "value", value, estimated: true };
167
+ return value === null ? { kind: "unavailable", reason: "invalid_duration" }
168
+ : { kind: "value", value, estimated: true, timingBasis: "generation-window" };
163
169
  }
164
170
  // A row that predates TTFT capture, or a non-streaming turn that never recorded one, has no
165
171
  // window to measure. That is a different fact from a bad duration, so it gets its own reason.
@@ -173,7 +179,7 @@ export function decodeTokPerSecondResult(
173
179
  if (windowMs < MIN_DECODE_WINDOW_MS) return { kind: "unavailable", reason: "decode_window_too_short" };
174
180
  const value = tokensPerSecond(entry.usage.outputTokens, windowMs);
175
181
  if (value === null) return { kind: "unavailable", reason: "invalid_duration" };
176
- return { kind: "value", value, estimated: true };
182
+ return { kind: "value", value, estimated: true, timingBasis: "legacy-post-visible-output" };
177
183
  }
178
184
 
179
185
  export function unavailableCostReason(entry: MetricSource): MetricUnavailableReason {
@@ -0,0 +1,60 @@
1
+ import {
2
+ AdmissionModelDeniedError, routeAllowedByScope, UNNAMED_DESTINATION_MODEL, type AdmissionModelScope,
3
+ } from "./admission-model-scope";
4
+
5
+ type Rec = Record<string, unknown>;
6
+ const isRec = (value: unknown): value is Rec => value !== null && typeof value === "object" && !Array.isArray(value);
7
+ const isAdvisor = (tool: Rec): boolean => typeof tool.type === "string" && /^advisor_\d{8}$/.test(tool.type);
8
+
9
+ /** Only executable declarations are policy inputs; schemas, arguments and results stay opaque. */
10
+ export function nativeMessagesToolScopeDenial(
11
+ scope: AdmissionModelScope | undefined,
12
+ providerName: string,
13
+ requestedModel: string,
14
+ body: Readonly<Rec>,
15
+ ): AdmissionModelDeniedError | undefined {
16
+ if (!scope || scope.models.length === 0) return undefined;
17
+ const definitions = new Map<string, Rec[]>();
18
+ const withdrawn = new Set<string>();
19
+ const anonymous: Rec[] = [];
20
+ if (Array.isArray(body.tools)) for (const tool of body.tools) {
21
+ if (!isRec(tool)) continue;
22
+ if (typeof tool.name !== "string") { anonymous.push(tool); continue; }
23
+ const declarations = definitions.get(tool.name) ?? [];
24
+ declarations.push(tool);
25
+ definitions.set(tool.name, declarations);
26
+ }
27
+ if (Array.isArray(body.messages)) for (const message of body.messages) {
28
+ if (!isRec(message) || message.role !== "system" || !Array.isArray(message.content)) continue;
29
+ // Upstream rejects tool changes in turn-scoped messages; do not depend on that. An addition
30
+ // always counts, and only a permanent removal withdraws a declaration (fail closed).
31
+ const permanent = message.clear_at === undefined || message.clear_at === "never";
32
+ for (const block of message.content) {
33
+ if (!isRec(block) || !isRec(block.tool)) continue;
34
+ const tool = block.tool;
35
+ if (block.type === "tool_addition" && tool.type === "tool_definition" && isRec(tool.definition)) {
36
+ const definition = tool.definition;
37
+ if (typeof definition.name !== "string") { anonymous.push(definition); continue; }
38
+ // A temporary replacement keeps the declaration it shadows in scope as well.
39
+ // These arrays belong to this check, not to the caller's declarations.
40
+ const declarations = permanent ? [] : definitions.get(definition.name) ?? [];
41
+ declarations.push(definition);
42
+ definitions.set(definition.name, declarations);
43
+ withdrawn.delete(definition.name);
44
+ } else if (tool.type === "tool_reference" && typeof tool.name === "string") {
45
+ if (block.type === "tool_removal" && permanent) withdrawn.add(tool.name);
46
+ if (block.type === "tool_addition") withdrawn.delete(tool.name);
47
+ }
48
+ }
49
+ }
50
+ // Deferred definitions can still be loaded by tool search in the current turn.
51
+ const active = [...anonymous, ...[...definitions].flatMap(([name, tools]) => withdrawn.has(name) ? [] : tools)];
52
+ for (const tool of active) {
53
+ if (!isAdvisor(tool)) continue;
54
+ const modelId = typeof tool.model === "string" && tool.model.trim() ? tool.model : undefined;
55
+ if (modelId === undefined || !routeAllowedByScope(scope, { providerName, modelId })) {
56
+ return new AdmissionModelDeniedError(requestedModel, { providerName, modelId: modelId ?? UNNAMED_DESTINATION_MODEL });
57
+ }
58
+ }
59
+ return undefined;
60
+ }
@@ -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
@@ -0,0 +1,14 @@
1
+ /** Restore the caller label while retaining JEV's forced effort and later child transitions. */
2
+ export function comboRequestedEffortLabel(
3
+ originalRequestedEffort: string | undefined,
4
+ childRequestedEffort: string | undefined,
5
+ forcedEffort?: string | null,
6
+ ): string | undefined {
7
+ if (originalRequestedEffort === undefined) return childRequestedEffort;
8
+ const transitionIndex = childRequestedEffort?.indexOf("->") ?? -1;
9
+ const laterTransitions = transitionIndex >= 0 ? childRequestedEffort!.slice(transitionIndex) : "";
10
+ const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
11
+ ? `->${forcedEffort}`
12
+ : "";
13
+ return `${originalRequestedEffort}${forcedTransition}${laterTransitions}`;
14
+ }
@@ -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).
@@ -106,6 +106,7 @@ import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
106
106
  import { settleOperatorReplacement } from "../../lib/upstream-retry";
107
107
  import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
108
108
  import { createJevModelInvoker } from "./jev-model-invoke";
109
+ import { comboRequestedEffortLabel } from "./combo-requested-effort";
109
110
  import { clientWireOf } from "../inference/client-wire";
110
111
 
111
112
  /**
@@ -656,13 +657,9 @@ export async function executeComboResponses(
656
657
  && isDeclaredReasoningEffort(originalRequestedEffortValue)
657
658
  ? originalRequestedEffortValue
658
659
  : undefined;
659
- const restoreOriginalRequestedEffort = (childLog: RequestLogContext): void => {
660
+ const restoreOriginalRequestedEffort = (childLog: RequestLogContext, forcedEffort?: string | null): void => {
660
661
  if (originalRequestedEffort === undefined) return;
661
- const normalizedRequestedEffort = childLog.requestedEffort;
662
- const transitionIndex = normalizedRequestedEffort?.indexOf("->") ?? -1;
663
- childLog.requestedEffort = transitionIndex >= 0
664
- ? `${originalRequestedEffort}${normalizedRequestedEffort!.slice(transitionIndex)}`
665
- : originalRequestedEffort;
662
+ childLog.requestedEffort = comboRequestedEffortLabel(originalRequestedEffort, childLog.requestedEffort, forcedEffort);
666
663
  recordAttemptRequestedEffort(childLog);
667
664
  };
668
665
 
@@ -907,7 +904,8 @@ export async function executeComboResponses(
907
904
  onNativePassthroughCancel: callbackGate.onCancel,
908
905
  onResponseComplete: callbackGate.onResponseComplete,
909
906
  });
910
- restoreOriginalRequestedEffort(childLog);
907
+ // Native Chat rebuilds its own body and does not apply the initial JEV effort.
908
+ restoreOriginalRequestedEffort(childLog, nativeChild ? undefined : initialJevDecision?.effort);
911
909
  } catch (error) {
912
910
  callbackGate.discard();
913
911
  if (options.abortSignal?.aborted) {
@@ -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
+ }