@bitkyc08/opencodex 2.22.0 → 2.23.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.
- package/gui/dist/assets/{index-ClEcVlFO.js → index-rFWrIE11.js} +19 -19
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +39 -7
- package/src/adapters/cursor/tool-definitions.ts +48 -0
- package/src/adapters/google.ts +18 -12
- package/src/adapters/openai-chat.ts +106 -13
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +3 -0
- package/src/bridge.ts +16 -5
- package/src/chat/inbound.ts +5 -11
- package/src/claude/context-windows.ts +5 -1
- package/src/claude/desktop-3p.ts +11 -6
- package/src/claude/inbound.ts +39 -1
- package/src/claude/model-info.ts +28 -8
- package/src/cli/account-api.ts +5 -1
- package/src/cli/claude-desktop.ts +3 -0
- package/src/cli/config-command.ts +37 -14
- package/src/codex/app-server-restart-service.ts +1 -1
- package/src/codex/auth-api.ts +5 -0
- package/src/codex/auth-context.ts +43 -2
- package/src/codex/catalog/metadata.ts +55 -8
- package/src/codex/catalog/native-models.ts +32 -2
- package/src/codex/catalog/parsing.ts +21 -7
- package/src/codex/catalog/provider-fetch.ts +35 -7
- package/src/codex/catalog/sync.ts +40 -18
- package/src/codex/catalog-refresh-status.ts +21 -3
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +23 -2
- package/src/codex/desired-state.ts +1 -1
- package/src/codex/inject.ts +38 -7
- package/src/codex/injected-marker.ts +28 -0
- package/src/codex/journal.ts +40 -1
- package/src/codex/management-convergence.ts +55 -2
- package/src/codex/quota-rejection.ts +61 -1
- package/src/codex/quota.ts +60 -6
- package/src/codex/routing.ts +30 -3
- package/src/combos/failover.ts +20 -0
- package/src/config.ts +271 -4
- package/src/generated/compatibility-version.json +86 -74
- package/src/grok/sync.ts +3 -1
- package/src/lab/artifacts/sanitize.ts +1 -1
- package/src/lab/live/manifest.ts +1 -1
- package/src/lib/codex-restart-contract.ts +1 -1
- package/src/lib/config-ownership.ts +1 -0
- package/src/lib/errors.ts +9 -0
- package/src/lib/lab-activation.ts +1 -1
- package/src/lib/optional-shutdown-hooks.ts +1 -1
- package/src/providers/quota.ts +10 -4
- package/src/providers/registry.ts +2 -2
- package/src/responses/parser.ts +42 -7
- package/src/responses/provider-opaque-metadata.ts +1 -1
- package/src/responses/thought-signature-replay.ts +261 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/provider-slot.ts +1 -1
- package/src/routing/evaluator.ts +12 -2
- package/src/routing/health.ts +16 -5
- package/src/routing/history/schema.ts +1 -1
- package/src/routing/trace.ts +1 -1
- package/src/server/auth-cors.ts +56 -21
- package/src/server/chat-completions.ts +6 -2
- package/src/server/index.ts +5 -3
- package/src/server/management/agent-settings-routes.ts +26 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/provider-routes.ts +19 -0
- package/src/server/management/shared.ts +3 -3
- package/src/server/management-api.ts +13 -6
- package/src/server/passive-route-linker.ts +1 -1
- package/src/server/relay.ts +16 -0
- package/src/server/responses/compact.ts +10 -3
- package/src/server/responses/core.ts +160 -33
- package/src/server/responses/fetch-helpers.ts +34 -2
- package/src/server/responses/input-admission.ts +17 -9
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/system-env.ts +4 -2
- package/src/service.ts +14 -7
- package/src/types.ts +34 -0
|
@@ -91,6 +91,8 @@ import {
|
|
|
91
91
|
CodexPoolAuthenticationError,
|
|
92
92
|
CodexThreadAffinityExpiredError,
|
|
93
93
|
headersForCodexAuthContext,
|
|
94
|
+
materializeCodexUpstreamAuth,
|
|
95
|
+
CodexMainSubstitutionUnavailableError,
|
|
94
96
|
isCodexAuthContextUsable,
|
|
95
97
|
resolveCodexAuthContext,
|
|
96
98
|
codexProbeLeaseId,
|
|
@@ -114,9 +116,11 @@ import {
|
|
|
114
116
|
prepareSameTarget429Wait,
|
|
115
117
|
} from "../../lib/upstream-retry";
|
|
116
118
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
|
|
119
|
+
import type { DataPlaneAdmission } from "../auth-cors";
|
|
117
120
|
import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
|
|
118
121
|
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
|
|
119
|
-
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
122
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
123
|
+
import { providerContextCap } from "../../providers/context-cap";
|
|
120
124
|
import { SERVICE_TIER_ADAPTERS, serviceTierSupportForModel } from "../../providers/service-tier";
|
|
121
125
|
import {
|
|
122
126
|
RequestPacingQueueOverloadError,
|
|
@@ -237,6 +241,13 @@ import {
|
|
|
237
241
|
} from "../sse-payload-rewrite";
|
|
238
242
|
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
239
243
|
import { createRoutedCustomToolRestoreBlockRewrite } from "../responses-custom-tool-repair";
|
|
244
|
+
import {
|
|
245
|
+
collectDeclaredWireToolNames,
|
|
246
|
+
createUndeclaredToolCallGuardBlockRewrite,
|
|
247
|
+
undeclaredToolCallMessage,
|
|
248
|
+
undeclaredToolCallName,
|
|
249
|
+
undeclaredToolCallNameInResponse,
|
|
250
|
+
} from "../responses-undeclared-tool-guard";
|
|
240
251
|
import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
|
|
241
252
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
242
253
|
import { guardTerminalEventStream } from "./terminal-guard";
|
|
@@ -457,6 +468,20 @@ type CodexPoolAccountRetryResult =
|
|
|
457
468
|
authCtx: Extract<CodexAuthContext, { kind: "pool" | "main-pool" }>;
|
|
458
469
|
};
|
|
459
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Workspace-denial evidence for a 403, read from the upstream body.
|
|
473
|
+
*
|
|
474
|
+
* #1789: a valid K12 credential gets 403 `codex_workspace_access_denied` on a routed prompt.
|
|
475
|
+
* Without this the account is quarantined for reauthentication, which cannot fix a workspace
|
|
476
|
+
* grant and loops forever. Fails closed: an unreadable body keeps the historical handling.
|
|
477
|
+
*/
|
|
478
|
+
async function codexDenialOutcomeMeta(response: Response): Promise<{ denial?: "workspace" | "entitlement" }> {
|
|
479
|
+
if (response.status !== 403) return {};
|
|
480
|
+
const { classifyCodexPreStreamRejection } = await import("../../codex/quota-rejection");
|
|
481
|
+
const rejection = await classifyCodexPreStreamRejection(response);
|
|
482
|
+
return rejection.denial ? { denial: rejection.denial } : {};
|
|
483
|
+
}
|
|
484
|
+
|
|
460
485
|
function codexQuotaOutcomeMeta(response: Response): {
|
|
461
486
|
retryAfter: string | null;
|
|
462
487
|
resetAt: string[];
|
|
@@ -521,7 +546,7 @@ async function retryCodexPoolOnAlternateAccount(
|
|
|
521
546
|
return { kind: "no-alternate" };
|
|
522
547
|
}
|
|
523
548
|
|
|
524
|
-
const quotaMeta = codexQuotaOutcomeMeta(firstResponse);
|
|
549
|
+
const quotaMeta = { ...codexQuotaOutcomeMeta(firstResponse), ...(await codexDenialOutcomeMeta(firstResponse)) };
|
|
525
550
|
if (outcomeStatus === 429 || outcomeStatus === 402) {
|
|
526
551
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
527
552
|
applyAccountQuotaFromUpstreamHeaders(
|
|
@@ -734,6 +759,15 @@ export interface ConsumedComboFailure {
|
|
|
734
759
|
|
|
735
760
|
export interface HandleResponsesOptions {
|
|
736
761
|
turnAdmissionLease?: AdmissionLease;
|
|
762
|
+
/**
|
|
763
|
+
* How the caller proved data-plane admission (#1686).
|
|
764
|
+
*
|
|
765
|
+
* A bearer-presented admission secret is one of OUR OWN secrets, so a Direct turn must
|
|
766
|
+
* SUBSTITUTE the stored main credential rather than forward it. Without this fact at the
|
|
767
|
+
* decision point, Direct cannot tell an admission bearer from the user own ChatGPT bearer,
|
|
768
|
+
* which is why it refused the whole env_key flow instead of serving it.
|
|
769
|
+
*/
|
|
770
|
+
admission?: DataPlaneAdmission;
|
|
737
771
|
/** Called at most once after the complete client body is read and accepted for dispatch. */
|
|
738
772
|
onRequestBodyRead?: () => void;
|
|
739
773
|
forceEmptyResponseId?: boolean;
|
|
@@ -967,7 +1001,14 @@ async function resolveResponsesCodexAuth(
|
|
|
967
1001
|
options: HandleResponsesOptions,
|
|
968
1002
|
): Promise<ResponsesAuthResolution> {
|
|
969
1003
|
try {
|
|
970
|
-
|
|
1004
|
+
// #1686: a caller that proved admission with a BEARER presented one of our own secrets.
|
|
1005
|
+
// Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
|
|
1006
|
+
// Admitting it is only safe because the stored main credential is substituted below, so
|
|
1007
|
+
// the admission secret still never leaves this process.
|
|
1008
|
+
const substituteMainCredential = options.admission?.source === "bearer";
|
|
1009
|
+
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
1010
|
+
validateForwardAdmissionCredential(req.headers, config);
|
|
1011
|
+
}
|
|
971
1012
|
let authCtx: CodexAuthContext;
|
|
972
1013
|
if (route.codexAccountMode) {
|
|
973
1014
|
authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
|
|
@@ -990,7 +1031,7 @@ async function resolveResponsesCodexAuth(
|
|
|
990
1031
|
return {
|
|
991
1032
|
ok: true,
|
|
992
1033
|
authCtx,
|
|
993
|
-
headers:
|
|
1034
|
+
headers: materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential }),
|
|
994
1035
|
};
|
|
995
1036
|
} catch (err) {
|
|
996
1037
|
if (err instanceof CodexAccountCooldownError) {
|
|
@@ -1024,6 +1065,13 @@ async function resolveResponsesCodexAuth(
|
|
|
1024
1065
|
if (err instanceof ForwardAdmissionCredentialError) {
|
|
1025
1066
|
return { ok: false, response: formatErrorResponse(401, "authentication_error", err.message) };
|
|
1026
1067
|
}
|
|
1068
|
+
if (err instanceof CodexMainSubstitutionUnavailableError) {
|
|
1069
|
+
// Fail BEFORE any upstream I/O. The alternative is forwarding the admission secret.
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
response: formatErrorResponse(401, "authentication_error", "No usable Codex main credential to serve this request"),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1027
1075
|
throw err;
|
|
1028
1076
|
}
|
|
1029
1077
|
}
|
|
@@ -1946,11 +1994,16 @@ async function handleResponsesInner(
|
|
|
1946
1994
|
// refusing the turn that shrinks the context would deadlock the client against the very
|
|
1947
1995
|
// limit this gate reports — it would be told to compact and then denied the compaction.
|
|
1948
1996
|
if (parsed._compactionRequest !== true) {
|
|
1949
|
-
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId);
|
|
1997
|
+
const inputAdmission = checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
1950
1998
|
if (!inputAdmission.admitted) {
|
|
1999
|
+
// #1524: this is a LOCAL preflight refusal, not an upstream verdict. A policy or combo
|
|
2000
|
+
// fallback must be able to skip this candidate and try one whose context window fits,
|
|
2001
|
+
// instead of treating the first incompatible candidate as the end of the chain. The
|
|
2002
|
+
// distinct code is what lets the fallback layer tell the two apart -- an upstream
|
|
2003
|
+
// `context_length_exceeded` still stops, because retrying it elsewhere is guesswork.
|
|
1951
2004
|
return formatErrorResponse(
|
|
1952
2005
|
413,
|
|
1953
|
-
"
|
|
2006
|
+
"input_admission_refused",
|
|
1954
2007
|
`Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
|
|
1955
2008
|
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
|
|
1956
2009
|
+ `model with a larger context window.`,
|
|
@@ -2280,6 +2333,69 @@ async function handleResponsesInner(
|
|
|
2280
2333
|
if (toolBridgeMaps.freeformToolNames.has(name)) routedCustomToolNames.add(name);
|
|
2281
2334
|
}
|
|
2282
2335
|
}
|
|
2336
|
+
// #1700: the bridged paths refuse a call to a tool the request never declared
|
|
2337
|
+
// (`declaredToolNames`, src/bridge.ts). The passthrough had no equivalent, so a routed
|
|
2338
|
+
// provider's top-level `apply_patch` — which under Codex code mode exists only as a nested
|
|
2339
|
+
// `tools.apply_patch(...)` helper inside `exec`, never as a wire tool — reached Codex as a
|
|
2340
|
+
// call it cannot execute, and the turn showed a bare `aborted` with the file untouched.
|
|
2341
|
+
// Forward auth is the canonical ChatGPT backend speaking Codex's own protocol rather than a
|
|
2342
|
+
// routed provider, so it keeps passing through unguarded, as it does for the rewrites above.
|
|
2343
|
+
// The guard needs a catalog to compare against, so it stands down when the request carries
|
|
2344
|
+
// none. That is not the same claim as "no tools means no tool may be called": a passthrough
|
|
2345
|
+
// request can legitimately omit `tools` entirely and still receive a tool call the client
|
|
2346
|
+
// understands — `tests/github-copilot-stream-contract.test.ts` sends `{model, input, stream}`
|
|
2347
|
+
// with no tools and Copilot answers with a `custom_tool_call` for `apply_patch`. Policing an
|
|
2348
|
+
// empty catalog truncates that turn. An unreadable body lands here too, since it yields no
|
|
2349
|
+
// names either.
|
|
2350
|
+
const outboundRequestBody = (() => {
|
|
2351
|
+
try {
|
|
2352
|
+
const body = JSON.parse(request.body) as unknown;
|
|
2353
|
+
return body && typeof body === "object" && !Array.isArray(body) ? body : undefined;
|
|
2354
|
+
} catch {
|
|
2355
|
+
return undefined;
|
|
2356
|
+
}
|
|
2357
|
+
})();
|
|
2358
|
+
const declaredWireToolNames = collectDeclaredWireToolNames(outboundRequestBody);
|
|
2359
|
+
// Union with the caller's own catalog, because the outbound body is not always a complete
|
|
2360
|
+
// record of it: hosted-tool preference REPLACES a client tool with its hosted form, so a
|
|
2361
|
+
// request declaring `image_gen.generate` ships `{type:"image_generation"}` upstream and gets
|
|
2362
|
+
// the client's name back. Widening only ever makes the guard fire less; a name declared in
|
|
2363
|
+
// neither place — #1700's `apply_patch` — is still refused.
|
|
2364
|
+
for (const name of toolBridgeMaps.declaredToolNames) declaredWireToolNames.add(name);
|
|
2365
|
+
const undeclaredToolGuardActive = declaredWireToolNames.size > 0
|
|
2366
|
+
&& route.provider.authMode !== "forward";
|
|
2367
|
+
// A refused turn must not seed `previous_response_id` replay. The inspection branch reads the
|
|
2368
|
+
// untouched upstream stream, so it can still observe a `response.completed` the client never
|
|
2369
|
+
// received; checking the payload itself rather than a flag shared with the client relay keeps
|
|
2370
|
+
// this free of tee ordering races.
|
|
2371
|
+
//
|
|
2372
|
+
// Checking only the terminal snapshot is not enough. An upstream can announce the undeclared
|
|
2373
|
+
// call in `response.output_item.added`, which trips the client guard, and then close with a
|
|
2374
|
+
// `response.completed` whose `output` is empty. The client gets `response.failed`, the terminal
|
|
2375
|
+
// check sees nothing undeclared, and the refused turn enters continuation state anyway. So the
|
|
2376
|
+
// rejection is sticky for the whole turn, set from every parsed payload on the inspection side.
|
|
2377
|
+
let inspectionSawUndeclaredTool = false;
|
|
2378
|
+
const noteInspectedPayload = (payload: unknown) => {
|
|
2379
|
+
// Gated on the same flag as the guard itself: with no readable catalog (or a forward-auth
|
|
2380
|
+
// provider) every name looks undeclared, and flipping this would stop recording continuation
|
|
2381
|
+
// state for exactly the passthrough traffic the guard deliberately stands down for.
|
|
2382
|
+
if (!undeclaredToolGuardActive || inspectionSawUndeclaredTool) return;
|
|
2383
|
+
if (undeclaredToolCallName(payload, declaredWireToolNames) !== undefined) {
|
|
2384
|
+
inspectionSawUndeclaredTool = true;
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
const rememberPassthroughResponseChecked = rememberPassthroughResponse
|
|
2388
|
+
? (response: { id?: unknown; output?: unknown; status?: unknown }) => {
|
|
2389
|
+
if (inspectionSawUndeclaredTool) return;
|
|
2390
|
+
if (
|
|
2391
|
+
undeclaredToolGuardActive
|
|
2392
|
+
&& undeclaredToolCallNameInResponse(response, declaredWireToolNames) !== undefined
|
|
2393
|
+
) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
rememberPassthroughResponse(response);
|
|
2397
|
+
}
|
|
2398
|
+
: undefined;
|
|
2283
2399
|
recordAdapterReasoning(logCtx, request);
|
|
2284
2400
|
const actualHostKey = upstreamHostHealthKey(
|
|
2285
2401
|
route.providerName,
|
|
@@ -2536,7 +2652,7 @@ async function handleResponsesInner(
|
|
|
2536
2652
|
if (usesCodexForwardPoolAuth(authCtx, route.provider)) {
|
|
2537
2653
|
// primary was the 5h window; it now carries weekly data for GPT plans.
|
|
2538
2654
|
// Prefer primary when present, fall back to secondary for compatibility.
|
|
2539
|
-
const quotaMeta = codexQuotaOutcomeMeta(upstreamResponse);
|
|
2655
|
+
const quotaMeta = { ...codexQuotaOutcomeMeta(upstreamResponse), ...(await codexDenialOutcomeMeta(upstreamResponse)) };
|
|
2540
2656
|
const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
|
|
2541
2657
|
applyAccountQuotaFromUpstreamHeaders(
|
|
2542
2658
|
authCtx.accountId,
|
|
@@ -2667,13 +2783,6 @@ async function handleResponsesInner(
|
|
|
2667
2783
|
// injection at the block level, after payload rewrites. Defaults come
|
|
2668
2784
|
// from the finalized OUTBOUND body — the normalized internal tool shapes
|
|
2669
2785
|
// are not the Responses wire shapes the snapshot must mirror.
|
|
2670
|
-
const snapshotDefaultsRequest = (() => {
|
|
2671
|
-
try {
|
|
2672
|
-
return JSON.parse(request.body) as unknown;
|
|
2673
|
-
} catch {
|
|
2674
|
-
return undefined;
|
|
2675
|
-
}
|
|
2676
|
-
})();
|
|
2677
2786
|
const blockRewrites = [
|
|
2678
2787
|
payloadRewrites.length > 0
|
|
2679
2788
|
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
@@ -2685,7 +2794,12 @@ async function handleResponsesInner(
|
|
|
2685
2794
|
? createGithubCopilotResponsesBlockRewrite(translatorBudget)
|
|
2686
2795
|
: undefined,
|
|
2687
2796
|
snapshotRepairEnabled
|
|
2688
|
-
? createResponsesSnapshotBlockRewrite(
|
|
2797
|
+
? createResponsesSnapshotBlockRewrite(outboundRequestBody, translatorBudget)
|
|
2798
|
+
: undefined,
|
|
2799
|
+
// Last: every rewrite above can still rename or reshape a call item, so the guard must
|
|
2800
|
+
// compare the names the client will actually receive against the declared catalog.
|
|
2801
|
+
undeclaredToolGuardActive
|
|
2802
|
+
? createUndeclaredToolCallGuardBlockRewrite(declaredWireToolNames)
|
|
2689
2803
|
: undefined,
|
|
2690
2804
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
2691
2805
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
@@ -2737,7 +2851,8 @@ async function handleResponsesInner(
|
|
|
2737
2851
|
const inspector = createSseInspector({
|
|
2738
2852
|
onTerminal: reportNativeTerminal,
|
|
2739
2853
|
logCtx,
|
|
2740
|
-
onCompletedResponse:
|
|
2854
|
+
onCompletedResponse: rememberPassthroughResponseChecked,
|
|
2855
|
+
onParsedPayload: noteInspectedPayload,
|
|
2741
2856
|
onFirstOutput: options.onFirstOutput,
|
|
2742
2857
|
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
2743
2858
|
});
|
|
@@ -2788,6 +2903,7 @@ async function handleResponsesInner(
|
|
|
2788
2903
|
drainBounds: { ms: 15_000, bytes: 32 * 1024 * 1024 },
|
|
2789
2904
|
upstream,
|
|
2790
2905
|
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
2906
|
+
onParsedPayload: noteInspectedPayload,
|
|
2791
2907
|
};
|
|
2792
2908
|
if (recordTerminalOutcomes) {
|
|
2793
2909
|
// A real terminal was parsed from the (teed) inspection stream — record it as the outcome
|
|
@@ -2821,7 +2937,7 @@ async function handleResponsesInner(
|
|
|
2821
2937
|
() => unregisterTurn(turnAc),
|
|
2822
2938
|
logCtx,
|
|
2823
2939
|
() => options.onNativePassthroughCancel?.(),
|
|
2824
|
-
|
|
2940
|
+
rememberPassthroughResponseChecked,
|
|
2825
2941
|
options.onFirstOutput,
|
|
2826
2942
|
inspectionConsumerOptions,
|
|
2827
2943
|
);
|
|
@@ -2831,7 +2947,7 @@ async function handleResponsesInner(
|
|
|
2831
2947
|
logCtx,
|
|
2832
2948
|
turnAc.signal,
|
|
2833
2949
|
() => unregisterTurn(turnAc),
|
|
2834
|
-
|
|
2950
|
+
rememberPassthroughResponseChecked,
|
|
2835
2951
|
options.onFirstOutput,
|
|
2836
2952
|
inspectionConsumerOptions,
|
|
2837
2953
|
);
|
|
@@ -2865,30 +2981,41 @@ async function handleResponsesInner(
|
|
|
2865
2981
|
}
|
|
2866
2982
|
const text = bounded.text;
|
|
2867
2983
|
inspectResponseLogJson(logCtx, text);
|
|
2868
|
-
if (rememberPassthroughResponse) {
|
|
2869
|
-
try {
|
|
2870
|
-
rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
|
|
2871
|
-
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
2872
|
-
}
|
|
2873
2984
|
const clientJson = (() => {
|
|
2874
2985
|
const restored = restoreRoutedCustomCallsInJson(
|
|
2875
2986
|
restoreImageGenCallsInJson(text, imageGenCallAliases),
|
|
2876
2987
|
routedCustomToolNames,
|
|
2877
2988
|
);
|
|
2878
|
-
const repaired = (
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
try {
|
|
2882
|
-
outbound = JSON.parse(request.body);
|
|
2883
|
-
} catch {
|
|
2884
|
-
outbound = undefined;
|
|
2885
|
-
}
|
|
2886
|
-
return repairResponsesSnapshotJson(restored, outbound);
|
|
2887
|
-
})();
|
|
2989
|
+
const repaired = hasResponsesSnapshotRepair(route.provider.responsesSnapshotRepair)
|
|
2990
|
+
? repairResponsesSnapshotJson(restored, outboundRequestBody)
|
|
2991
|
+
: restored;
|
|
2888
2992
|
return parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
|
|
2889
2993
|
? rewriteResponsesModelJson(repaired, parsed._responseModelId)
|
|
2890
2994
|
: repaired;
|
|
2891
2995
|
})();
|
|
2996
|
+
// #1700: same fail-closed policy as the SSE relay above. Both the plain JSON answer and
|
|
2997
|
+
// the reframed-SSE branch below are built from this body, so one check covers them. This
|
|
2998
|
+
// runs BEFORE the continuation cache write below: a refused turn must not become state a
|
|
2999
|
+
// later `previous_response_id` replay can expand from.
|
|
3000
|
+
if (undeclaredToolGuardActive) {
|
|
3001
|
+
const undeclared = (() => {
|
|
3002
|
+
try {
|
|
3003
|
+
return undeclaredToolCallNameInResponse(JSON.parse(clientJson), declaredWireToolNames);
|
|
3004
|
+
} catch {
|
|
3005
|
+
return undefined;
|
|
3006
|
+
}
|
|
3007
|
+
})();
|
|
3008
|
+
if (undeclared !== undefined) {
|
|
3009
|
+
return formatErrorResponse(502, "upstream_error", undeclaredToolCallMessage(undeclared));
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (rememberPassthroughResponseChecked) {
|
|
3013
|
+
try {
|
|
3014
|
+
rememberPassthroughResponseChecked(
|
|
3015
|
+
JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown },
|
|
3016
|
+
);
|
|
3017
|
+
} catch { /* non-JSON despite content-type; recording is best-effort */ }
|
|
3018
|
+
}
|
|
2892
3019
|
// #875: the transport-neutral reliability policy forced a bounded JSON
|
|
2893
3020
|
// upstream for a client that asked for SSE. Reframe the completed JSON
|
|
2894
3021
|
// as the canonical terminal SSE sequence (created → output_item.done →
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import { isInjectionDebugEnabled } from "../../lib/debug-settings";
|
|
34
34
|
import { injectionDebugLog } from "../../lib/injection-debug-log";
|
|
35
35
|
import { modelInList, namespacedToolName } from "../../types";
|
|
36
|
-
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
|
|
36
|
+
import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage, UpstreamHttpVersion } from "../../types";
|
|
37
37
|
import {
|
|
38
38
|
forceRefreshOAuthAccessSnapshot,
|
|
39
39
|
getOAuthCredentialApiBaseUrl,
|
|
@@ -149,6 +149,38 @@ export interface ProviderFetchOptions {
|
|
|
149
149
|
modelId?: string;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Bun's fetch accepts a non-standard `protocol` init to pin the HTTP version
|
|
154
|
+
* (BunFetchRequestInit.protocol). The DOM lib types do not include it, so the
|
|
155
|
+
* value is carried on an intersection and stripped before non-Bun callers.
|
|
156
|
+
* The accepted values are the shared UPSTREAM_HTTP_VERSION_VALUES enum from types.
|
|
157
|
+
*/
|
|
158
|
+
const UPSTREAM_HTTP_VERSION_PROTOCOL: Record<Exclude<UpstreamHttpVersion, "auto">, string> = {
|
|
159
|
+
"http1.1": "http1.1",
|
|
160
|
+
h1: "h1",
|
|
161
|
+
http2: "http2",
|
|
162
|
+
h2: "h2",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Attach Bun's `protocol` pin when the provider opted into a fixed HTTP version. */
|
|
166
|
+
export function withUpstreamHttpVersion(
|
|
167
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
168
|
+
init: RequestInit | undefined,
|
|
169
|
+
provider: OcxProviderConfig,
|
|
170
|
+
): RequestInit | undefined {
|
|
171
|
+
const version = provider.upstreamHttpVersion;
|
|
172
|
+
if (!version || version === "auto") return init;
|
|
173
|
+
// Bun's protocol pin requires an https: target; local/plaintext upstreams keep
|
|
174
|
+
// their existing transport untouched.
|
|
175
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
176
|
+
try {
|
|
177
|
+
if (new URL(target).protocol !== "https:") return init;
|
|
178
|
+
} catch {
|
|
179
|
+
return init;
|
|
180
|
+
}
|
|
181
|
+
return { ...(init ?? {}), protocol: UPSTREAM_HTTP_VERSION_PROTOCOL[version] } as RequestInit;
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
export function providerFetch(
|
|
153
185
|
provider: OcxProviderConfig,
|
|
154
186
|
runtime: BunRuntimeGateInput = currentBunRuntimeIdentity(),
|
|
@@ -162,7 +194,7 @@ export function providerFetch(
|
|
|
162
194
|
if (typeof input === "string" && init && shouldUseCodexWsUpstream(input, init, runtime)) {
|
|
163
195
|
return codexWsUpstreamFetch(input, init, base, runtime);
|
|
164
196
|
}
|
|
165
|
-
return base(input, init);
|
|
197
|
+
return base(input, withUpstreamHttpVersion(input, init, provider));
|
|
166
198
|
};
|
|
167
199
|
const waitForPacing = (signal?: AbortSignal) => options.providerName
|
|
168
200
|
? waitForProviderRequestSlot(options.providerName, provider, options.modelId, signal)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* catches the pathological case and stays out of the way otherwise. Every uncertainty
|
|
11
11
|
* resolves toward admitting.
|
|
12
12
|
*/
|
|
13
|
-
import { nativeOpenAiContextWindow } from "../../codex/catalog/metadata";
|
|
13
|
+
import { nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens } from "../../codex/catalog/metadata";
|
|
14
14
|
import { estimateTokens } from "../../lib/token-estimate";
|
|
15
15
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
16
16
|
import type { OcxContentPart, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
@@ -129,6 +129,9 @@ export function resolveInputCeiling(
|
|
|
129
129
|
provider: OcxProviderConfig,
|
|
130
130
|
providerName: string,
|
|
131
131
|
modelId: string,
|
|
132
|
+
// Operator cap for the canonical native provider. Passed in rather than read from a
|
|
133
|
+
// config here so this stays pure: no filesystem, no catalog, no registry scan.
|
|
134
|
+
nativeContextCap?: number,
|
|
132
135
|
): number | null {
|
|
133
136
|
const configured = positive(provider.modelContextWindows?.[modelId]) ?? positive(provider.contextWindow);
|
|
134
137
|
|
|
@@ -137,18 +140,22 @@ export function resolveInputCeiling(
|
|
|
137
140
|
// transport-mismatched custom provider named "openai" is preserved verbatim by routing
|
|
138
141
|
// and must not inherit built-in native limits, and a routed `provider/model` id is not a
|
|
139
142
|
// native slug. Static maps only — no catalog read.
|
|
140
|
-
const
|
|
141
|
-
&& providerName === OPENAI_CODEX_PROVIDER_ID
|
|
143
|
+
const canonicalNativeBare = providerName === OPENAI_CODEX_PROVIDER_ID
|
|
142
144
|
&& isCanonicalOpenAiForwardProvider(provider)
|
|
143
|
-
&& !modelId.includes("/")
|
|
144
|
-
|
|
145
|
+
&& !modelId.includes("/");
|
|
146
|
+
const native = configured === null && canonicalNativeBare
|
|
147
|
+
? positive(nativeOpenAiContextWindow(modelId, nativeContextCap))
|
|
145
148
|
: null;
|
|
149
|
+
// The model's own measured input ceiling applies even when the operator configured a
|
|
150
|
+
// window: a 1,050,000 override must not raise the gate above the 922,000 the upstream
|
|
151
|
+
// actually accepts. Computed independently of `configured` for exactly that reason.
|
|
152
|
+
const nativeMaxInput = canonicalNativeBare ? positive(nativeOpenAiMaxInputTokens(modelId, nativeContextCap)) : null;
|
|
146
153
|
|
|
147
154
|
const window = configured ?? native;
|
|
148
155
|
// modelMaxInputTokens is an input-only cap, so it can only tighten the window.
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
return
|
|
156
|
+
const configuredMaxInput = positive(provider.modelMaxInputTokens?.[modelId]);
|
|
157
|
+
const limits = [window, configuredMaxInput, nativeMaxInput].filter((v): v is number => v !== null);
|
|
158
|
+
return limits.length === 0 ? null : Math.min(...limits);
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
/**
|
|
@@ -161,8 +168,9 @@ export function checkInputAdmission(
|
|
|
161
168
|
provider: OcxProviderConfig,
|
|
162
169
|
providerName: string,
|
|
163
170
|
modelId: string,
|
|
171
|
+
nativeContextCap?: number,
|
|
164
172
|
): InputAdmissionResult {
|
|
165
|
-
const ceiling = resolveInputCeiling(provider, providerName, modelId);
|
|
173
|
+
const ceiling = resolveInputCeiling(provider, providerName, modelId, nativeContextCap);
|
|
166
174
|
if (ceiling === null) return { admitted: true, estimatedTokens: 0, ceiling: null };
|
|
167
175
|
const estimatedTokens = estimateInputTokens(parsed, modelId);
|
|
168
176
|
return { admitted: estimatedTokens <= ceiling * ADMISSION_TOLERANCE, estimatedTokens, ceiling };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { namespacedToolName } from "../types";
|
|
2
|
+
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
|
|
6
|
+
* `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
|
|
7
|
+
* carry no tool name, so they are never matched against the request catalog.
|
|
8
|
+
*/
|
|
9
|
+
const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
|
|
10
|
+
|
|
11
|
+
/** An upstream-supplied name reaches the error message; keep it bounded. */
|
|
12
|
+
const MAX_REPORTED_NAME_CHARS = 100;
|
|
13
|
+
|
|
14
|
+
export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
|
|
15
|
+
|
|
16
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
|
|
21
|
+
if (!isPlainObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
|
|
22
|
+
names.add(tool.name);
|
|
23
|
+
// Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
|
|
24
|
+
// as a bare inner name or as the flattened form; accept both rather than guess which
|
|
25
|
+
// coordinate system this provider echoes back.
|
|
26
|
+
if (namespace) names.add(namespacedToolName(namespace, tool.name));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addWireToolSpecs(names: Set<string>, specs: unknown): void {
|
|
30
|
+
if (!Array.isArray(specs)) return;
|
|
31
|
+
for (const spec of specs) {
|
|
32
|
+
if (!isPlainObject(spec)) continue;
|
|
33
|
+
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
34
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
35
|
+
for (const inner of spec.tools) addWireToolName(names, inner, namespace);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
addWireToolName(names, spec);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tool names the OUTBOUND Responses body actually declared.
|
|
44
|
+
*
|
|
45
|
+
* This reads the body that goes upstream rather than the parsed internal tool list: the
|
|
46
|
+
* passthrough forwards wire shapes (namespaced MCP groups, `additional_tools` items carried
|
|
47
|
+
* inside `input`, routed custom-tool rewrites) that the internal list flattens or renames, and
|
|
48
|
+
* only the wire names can be compared against what the provider echoes back.
|
|
49
|
+
*/
|
|
50
|
+
export function collectDeclaredWireToolNames(body: unknown): Set<string> {
|
|
51
|
+
const names = new Set<string>();
|
|
52
|
+
if (!isPlainObject(body)) return names;
|
|
53
|
+
addWireToolSpecs(names, body.tools);
|
|
54
|
+
if (Array.isArray(body.input)) {
|
|
55
|
+
for (const item of body.input) {
|
|
56
|
+
if (isPlainObject(item) && item.type === "additional_tools") addWireToolSpecs(names, item.tools);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return names;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): string | undefined {
|
|
63
|
+
if (!isPlainObject(item)) return undefined;
|
|
64
|
+
if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
|
|
65
|
+
const name = item.name;
|
|
66
|
+
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
67
|
+
if (declared.has(name)) return undefined;
|
|
68
|
+
if (typeof item.namespace === "string" && declared.has(namespacedToolName(item.namespace, name))) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** First undeclared client tool named by a Responses SSE payload, or undefined. */
|
|
75
|
+
export function undeclaredToolCallName(
|
|
76
|
+
payload: unknown,
|
|
77
|
+
declared: ReadonlySet<string>,
|
|
78
|
+
): string | undefined {
|
|
79
|
+
if (!isPlainObject(payload)) return undefined;
|
|
80
|
+
if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
|
|
81
|
+
return undeclaredNameInItem(payload.item, declared);
|
|
82
|
+
}
|
|
83
|
+
// Sparse gateways skip incremental items and only ever ship the terminal snapshot.
|
|
84
|
+
if (payload.type === "response.completed" || payload.type === "response.incomplete") {
|
|
85
|
+
return undeclaredToolCallNameInResponse(payload.response, declared);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** First undeclared client tool in a Responses object's `output` array, or undefined. */
|
|
91
|
+
export function undeclaredToolCallNameInResponse(
|
|
92
|
+
response: unknown,
|
|
93
|
+
declared: ReadonlySet<string>,
|
|
94
|
+
): string | undefined {
|
|
95
|
+
if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
|
|
96
|
+
for (const item of response.output) {
|
|
97
|
+
const name = undeclaredNameInItem(item, declared);
|
|
98
|
+
if (name !== undefined) return name;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function undeclaredToolCallMessage(name: string): string {
|
|
104
|
+
const reported = name.slice(0, MAX_REPORTED_NAME_CHARS);
|
|
105
|
+
return `routed provider emitted undeclared client tool "${reported}"; only request-declared tools may be called`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function failedBlocks(name: string, newline: string): readonly string[] {
|
|
109
|
+
const failure = {
|
|
110
|
+
type: "upstream_error",
|
|
111
|
+
code: UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
112
|
+
message: undeclaredToolCallMessage(name),
|
|
113
|
+
};
|
|
114
|
+
const payload = JSON.stringify({
|
|
115
|
+
type: "response.failed",
|
|
116
|
+
response: { status: "failed", error: failure, last_error: failure },
|
|
117
|
+
});
|
|
118
|
+
return [`event: response.failed${newline}data: ${payload}`, "data: [DONE]"];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fail closed when a routed provider calls a tool the request never declared (#1700).
|
|
123
|
+
*
|
|
124
|
+
* The bridged paths already refuse such a call (`declaredToolNames` in src/bridge.ts), but the
|
|
125
|
+
* native Responses passthrough relayed it verbatim: Codex received a `function_call` for a tool
|
|
126
|
+
* it has no top-level handler for — `apply_patch`, which under code mode exists only as a nested
|
|
127
|
+
* `tools.apply_patch(...)` helper inside `exec` — and the turn surfaced as a bare `aborted` with
|
|
128
|
+
* no output and no explanation. Replacing the offending event with an explicit `response.failed`
|
|
129
|
+
* turns that silent dead end into a compatibility error naming the tool.
|
|
130
|
+
*
|
|
131
|
+
* Everything after the trip is dropped so a later `response.completed` cannot contradict the
|
|
132
|
+
* terminal already sent. Non-JSON and non-item blocks pass through untouched.
|
|
133
|
+
*/
|
|
134
|
+
export function createUndeclaredToolCallGuardBlockRewrite(
|
|
135
|
+
declared: ReadonlySet<string>,
|
|
136
|
+
): SseBlockRewrite {
|
|
137
|
+
let tripped = false;
|
|
138
|
+
return (block: string) => {
|
|
139
|
+
if (tripped) return [];
|
|
140
|
+
const payload = sseDataPayload(block);
|
|
141
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
142
|
+
let parsed: unknown;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(payload);
|
|
145
|
+
} catch {
|
|
146
|
+
return [block];
|
|
147
|
+
}
|
|
148
|
+
const name = undeclaredToolCallName(parsed, declared);
|
|
149
|
+
if (name === undefined) return [block];
|
|
150
|
+
tripped = true;
|
|
151
|
+
return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/server/system-env.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionToke
|
|
|
7
7
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
|
+
import { providerContextCap } from "../providers/context-cap";
|
|
11
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Does the opencodex dummy marker belong in the system environment?
|
|
@@ -237,10 +239,10 @@ async function computeEffectiveModelEnv(config: OcxConfig, auto?: AutoContextMod
|
|
|
237
239
|
const windows = await boundedContextWindows(async () => {
|
|
238
240
|
const { gatherRoutedModels, visibleNativeSlugs } = await import("../codex/catalog");
|
|
239
241
|
try {
|
|
240
|
-
return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config));
|
|
242
|
+
return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config), providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
241
243
|
} catch (error) {
|
|
242
244
|
if (error && typeof error === "object" && (error as { code?: unknown }).code === "catalog_busy") {
|
|
243
|
-
return buildClaudeContextWindows([...visibleNativeSlugs(config)], []);
|
|
245
|
+
return buildClaudeContextWindows([...visibleNativeSlugs(config)], [], providerContextCap(config, OPENAI_CODEX_PROVIDER_ID));
|
|
244
246
|
}
|
|
245
247
|
throw error;
|
|
246
248
|
}
|
package/src/service.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { execFileSync, execSync, spawnSync } from "node:child_process";
|
|
|
9
9
|
import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
|
|
10
10
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir, tmpdir } from "node:os";
|
|
12
|
-
import { dirname, join, resolve, win32 } from "node:path";
|
|
12
|
+
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
13
13
|
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
14
14
|
import { loadConfig } from "./config";
|
|
15
15
|
import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
|
|
@@ -113,12 +113,19 @@ function currentCodexSqliteHomeAbsolute(target: "native" | "windows" = "native")
|
|
|
113
113
|
const raw = process.env.CODEX_SQLITE_HOME?.trim();
|
|
114
114
|
if (!raw) return undefined;
|
|
115
115
|
const expanded = expandUserPath(raw);
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
// Service artifacts can be rendered by cross-platform tests and repair tooling, so an
|
|
117
|
+
// already-absolute path for the TARGET platform is preserved rather than re-anchored
|
|
118
|
+
// against the writing host. `resolve()` is host-relative in both directions: on a POSIX
|
|
119
|
+
// host it turns `C:\data` into `<cwd>/C:\data`, and on a Windows host it turns `/tmp/x`
|
|
120
|
+
// into `D:\tmp\x` — neither is a path the target can use. A relative value still resolves,
|
|
121
|
+
// because a service unit has no meaningful working directory.
|
|
122
|
+
//
|
|
123
|
+
// CODEX_HOME and OPENCODEX_HOME are carried through literally, so without this the same
|
|
124
|
+
// generated file disagreed with itself about two variables holding the same kind of value.
|
|
125
|
+
if (target === "windows") {
|
|
126
|
+
return win32.isAbsolute(expanded) ? win32.normalize(expanded) : resolve(expanded);
|
|
127
|
+
}
|
|
128
|
+
return posix.isAbsolute(expanded) ? posix.normalize(expanded) : resolve(expanded);
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
function currentOpenCodexHome(): string {
|