@bitkyc08/opencodex 2.64.0-preview.20260923 → 2.65.0-preview.20260925
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/README.md +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-BEq4OCOz.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-D5eN1TID.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-Bh_ErQDh.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +0 -1
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./main-account";
|
|
29
29
|
import { isMainAccountPolicyBindingPending, isNativeMainTrafficBlocked, nativeMainStartupGateSnapshot } from "./native-profile-startup";
|
|
30
30
|
import type { NativeMainStartupBlockReason } from "./native-profile-startup";
|
|
31
|
+
import { waitForMainAccountPolicyBinding } from "./main-account-policy-wait";
|
|
31
32
|
import {
|
|
32
33
|
codexQuotaScopeForModel,
|
|
33
34
|
computeCodexUsageScore,
|
|
@@ -67,7 +68,12 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
|
67
68
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
68
69
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
69
70
|
import { extractAccountId, extractEmail } from "../oauth/chatgpt";
|
|
70
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
MAIN_ACCOUNT_HARD_LOCK_PERCENT,
|
|
73
|
+
getMainAccountHardLockStatus,
|
|
74
|
+
isMainAccountHardLockEnabled,
|
|
75
|
+
isMainAccountHardLocked,
|
|
76
|
+
} from "./main-account-hard-lock";
|
|
71
77
|
import {
|
|
72
78
|
captureMainAccountIdentityGeneration,
|
|
73
79
|
captureMainQuotaWriter,
|
|
@@ -508,7 +514,7 @@ export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
|
|
|
508
514
|
super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
|
|
509
515
|
this.name = "CodexMainAccountHardLockError";
|
|
510
516
|
this.resetAt = resetAt;
|
|
511
|
-
this.message =
|
|
517
|
+
this.message = `Codex main account is blocked by the ${MAIN_ACCOUNT_HARD_LOCK_PERCENT}% main-account quota policy.`
|
|
512
518
|
+ " Choose another account, wait for quota to reset, or disable codexMainAccountHardLock in Settings.";
|
|
513
519
|
}
|
|
514
520
|
}
|
|
@@ -898,6 +904,8 @@ export interface ResolveCodexAuthContextOptions {
|
|
|
898
904
|
getValidMainAccountToken?: typeof getValidMainAccountToken;
|
|
899
905
|
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
900
906
|
signal?: AbortSignal;
|
|
907
|
+
/** Test seam: overrides the bounded startup policy-binding wait before the hard-lock fence. */
|
|
908
|
+
mainAccountPolicyBindingWaitMs?: number;
|
|
901
909
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
902
910
|
/** Test seam for account-gated native model discovery. */
|
|
903
911
|
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
@@ -920,6 +928,32 @@ export interface CodexAccountSelectionAdmission {
|
|
|
920
928
|
release(): void;
|
|
921
929
|
}
|
|
922
930
|
|
|
931
|
+
/**
|
|
932
|
+
* Wait, bounded, for an owned startup's main-policy binding, then fail closed while it is still
|
|
933
|
+
* in flight.
|
|
934
|
+
*
|
|
935
|
+
* #5694 turned `codexMainAccountHardLock` on by default, so this fence stopped being an opt-in
|
|
936
|
+
* rarity and started intercepting ordinary requests: for the seconds a Windows startup spends
|
|
937
|
+
* recovering, sweeping stages, and binding the pinned home, every caller-owned direct request
|
|
938
|
+
* was answered with a 503 "native-main profile maintenance is active; retry". Waiting is the
|
|
939
|
+
* right answer for a window that closes on its own -- the request resumes and the hard lock
|
|
940
|
+
* below still decides on identity and quota, exactly as it does after startup. A gate still
|
|
941
|
+
* pending at the deadline (retained recovery, a manual-recovery requirement) is a real refusal,
|
|
942
|
+
* and that is the one case that keeps the draining error.
|
|
943
|
+
*
|
|
944
|
+
* Both reads are memory-only: neither this fence nor the wait probes a foreign home.
|
|
945
|
+
*/
|
|
946
|
+
async function awaitMainAccountPolicyBindingSettled(
|
|
947
|
+
options: ResolveCodexAuthContextOptions,
|
|
948
|
+
): Promise<void> {
|
|
949
|
+
if (!isMainAccountPolicyBindingPending()) return;
|
|
950
|
+
const settled = await waitForMainAccountPolicyBinding({
|
|
951
|
+
signal: options.signal,
|
|
952
|
+
timeoutMs: options.mainAccountPolicyBindingWaitMs,
|
|
953
|
+
});
|
|
954
|
+
if (!settled) throw new CodexMainProfileDrainingError();
|
|
955
|
+
}
|
|
956
|
+
|
|
923
957
|
export async function resolveCodexAuthContext(
|
|
924
958
|
headers: Headers,
|
|
925
959
|
config: OcxConfig,
|
|
@@ -956,9 +990,11 @@ export async function resolveCodexAuthContext(
|
|
|
956
990
|
);
|
|
957
991
|
const requestOwnedMainPinCandidate = mainPinState().candidate;
|
|
958
992
|
// During an owned startup, equality cannot be established until recovery and the
|
|
959
|
-
// memory-only policy binding finish. This read-only fence never probes a foreign home
|
|
960
|
-
|
|
961
|
-
|
|
993
|
+
// memory-only policy binding finish. This read-only fence never probes a foreign home, and it
|
|
994
|
+
// waits out a window that closes on its own instead of refusing a request that arrived inside
|
|
995
|
+
// it. Only a binding still pending at the deadline fails closed.
|
|
996
|
+
if (isMainAccountHardLockEnabled(policy) && requestOwnedMainPinCandidate) {
|
|
997
|
+
await awaitMainAccountPolicyBindingSettled(options);
|
|
962
998
|
}
|
|
963
999
|
const preserveRequestOwnedMainPin = () => mainPinState().preserve;
|
|
964
1000
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
@@ -970,9 +1006,7 @@ export async function resolveCodexAuthContext(
|
|
|
970
1006
|
// Trusted substitution still has to claim and validate stored main below.
|
|
971
1007
|
if (!substituteStoredMain && !hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
972
1008
|
if (!substituteStoredMain) {
|
|
973
|
-
if (policy
|
|
974
|
-
throw new CodexMainProfileDrainingError();
|
|
975
|
-
}
|
|
1009
|
+
if (isMainAccountHardLockEnabled(policy)) await awaitMainAccountPolicyBindingSettled(options);
|
|
976
1010
|
if (callerMatchesObservedMain(headers)) assertMainAccountPolicy(policy);
|
|
977
1011
|
assertCallerOwnedMainPoolNotCooled();
|
|
978
1012
|
if (reserve) {
|
|
@@ -1014,7 +1048,7 @@ export async function resolveCodexAuthContext(
|
|
|
1014
1048
|
) {
|
|
1015
1049
|
throw new CodexMainProfileDrainingError();
|
|
1016
1050
|
}
|
|
1017
|
-
if (policy
|
|
1051
|
+
if (isMainAccountHardLockEnabled(policy)) reconcileMainCodexAccountRuntimeState();
|
|
1018
1052
|
assertMainAccountPolicy(policy);
|
|
1019
1053
|
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
1020
1054
|
const entitled = entitledCodexAccountIdsForModel(
|
|
@@ -1542,7 +1576,11 @@ export function materializeCodexUpstreamAuth(
|
|
|
1542
1576
|
if (accountId) selected.set("chatgpt-account-id", accountId);
|
|
1543
1577
|
}
|
|
1544
1578
|
if (ctx.kind === "main" && options.substituteMainCredential === true) {
|
|
1545
|
-
|
|
1579
|
+
// No config object is "no policy input", not the persisted opt-out: only a config that
|
|
1580
|
+
// actually says `false` disables the default-on lock (#5694).
|
|
1581
|
+
if (options.config !== undefined && isMainAccountHardLockEnabled(options.config)) {
|
|
1582
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1583
|
+
}
|
|
1546
1584
|
const writer = captureObservedMainWriter();
|
|
1547
1585
|
const stored = getMainAccountToken();
|
|
1548
1586
|
// Fail BEFORE any upstream I/O. Falling through here would send the admission secret.
|
|
@@ -1620,7 +1658,9 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1620
1658
|
const value = headers.get(name);
|
|
1621
1659
|
if (value) selected.set(name, value);
|
|
1622
1660
|
}
|
|
1623
|
-
if (options.config
|
|
1661
|
+
if (options.config !== undefined && isMainAccountHardLockEnabled(options.config)) {
|
|
1662
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1663
|
+
}
|
|
1624
1664
|
const writer = captureObservedMainWriter();
|
|
1625
1665
|
const stored = await getValidMainAccountToken({
|
|
1626
1666
|
signal: options.signal,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import { effectiveProviderAlias } from "../../providers/default-aliases";
|
|
3
|
-
import {
|
|
3
|
+
import { neutralizeIdentity } from "../../adapters/identity";
|
|
4
4
|
import { COMBO_NAMESPACE } from "../../combos";
|
|
5
5
|
import {
|
|
6
6
|
CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
@@ -145,13 +145,13 @@ export function deriveEntry(
|
|
|
145
145
|
delete e.max_context_window;
|
|
146
146
|
delete e.auto_compact_token_limit;
|
|
147
147
|
}
|
|
148
|
-
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
149
|
-
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
150
|
-
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
151
148
|
if (typeof e.base_instructions === "string") {
|
|
152
149
|
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
|
|
153
150
|
// (leaking that into base_instructions is a non-first-party signature → ToS risk).
|
|
154
|
-
|
|
151
|
+
// Model-neutral on disk (#5217): Codex stores this block as the session's instructions and
|
|
152
|
+
// replays it verbatim into a sub-agent spawned on a DIFFERENT model, so a baked-in model id
|
|
153
|
+
// follows the worker and misnames it. The destination id is written at request time instead.
|
|
154
|
+
e.base_instructions = neutralizeIdentity(e.base_instructions);
|
|
155
155
|
}
|
|
156
156
|
applyReasoningLevels(
|
|
157
157
|
e,
|
|
@@ -25,11 +25,13 @@ import {
|
|
|
25
25
|
} from "../model-cache";
|
|
26
26
|
import {
|
|
27
27
|
buildModelsRequest,
|
|
28
|
+
getOAuthCredentialApiBaseUrl,
|
|
28
29
|
getValidAccessTokenSnapshot,
|
|
29
30
|
observeActiveOAuthAccessToken,
|
|
30
31
|
resolveModelsAuthToken,
|
|
31
32
|
type OAuthActiveTokenObservation,
|
|
32
33
|
} from "../../oauth";
|
|
34
|
+
import { getAccountSet } from "../../oauth/store";
|
|
33
35
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
34
36
|
import { modelInList } from "../../types";
|
|
35
37
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
@@ -150,6 +152,12 @@ export interface CapturedProviderGather {
|
|
|
150
152
|
readonly metadataModelIdCaseFold: boolean;
|
|
151
153
|
readonly effectiveAlias?: string | null;
|
|
152
154
|
readonly observedAuth?: ModelsAuthResolution;
|
|
155
|
+
/**
|
|
156
|
+
* The active OAuth account a refreshing capture was taken under. It is part of the
|
|
157
|
+
* flight's auth identity, so a caller on another account never joins a pending
|
|
158
|
+
* discovery started for this one, even when both accounts share an API host.
|
|
159
|
+
*/
|
|
160
|
+
readonly refreshingOAuthAccountId?: string;
|
|
153
161
|
/**
|
|
154
162
|
* Configured model ids this provider must keep even when live discovery omits
|
|
155
163
|
* them — combo targets that are also listed in providers.*.models (OCX-111).
|
|
@@ -315,14 +323,12 @@ export function captureTrustedOpenAiApiPolicy(
|
|
|
315
323
|
});
|
|
316
324
|
}
|
|
317
325
|
|
|
318
|
-
function captureModelsRequest(
|
|
326
|
+
export function captureModelsRequest(
|
|
319
327
|
name: string,
|
|
320
328
|
provider: OcxProviderConfig,
|
|
321
|
-
|
|
329
|
+
oauthApiBaseUrl: string | undefined,
|
|
322
330
|
): CapturedModelsRequest {
|
|
323
|
-
const observed =
|
|
324
|
-
? { oauthApiBaseUrl: observedAuth.oauthApiBaseUrl }
|
|
325
|
-
: undefined;
|
|
331
|
+
const observed = { oauthApiBaseUrl };
|
|
326
332
|
const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
|
|
327
333
|
const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
|
|
328
334
|
const method = withoutCredential.method ?? "GET";
|
|
@@ -378,7 +384,18 @@ export function captureProviderGather(
|
|
|
378
384
|
&& provider.liveModels !== false
|
|
379
385
|
? authResolver.resolve(name, provider)
|
|
380
386
|
: undefined;
|
|
381
|
-
|
|
387
|
+
// A refreshing capture carries the stored origin so accounts on different hosts keep separate
|
|
388
|
+
// flights. The send is rebuilt from the auth the gather resolves, and the observed path never
|
|
389
|
+
// reads the live store.
|
|
390
|
+
const oauthApiBaseUrl = observedAuth
|
|
391
|
+
? observedAuth.oauthApiBaseUrl
|
|
392
|
+
: authResolver.kind === "refreshing" && provider.authMode === "oauth"
|
|
393
|
+
? getOAuthCredentialApiBaseUrl(name)
|
|
394
|
+
: undefined;
|
|
395
|
+
const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
|
|
396
|
+
const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth"
|
|
397
|
+
? getAccountSet(name)?.activeAccountId
|
|
398
|
+
: undefined;
|
|
382
399
|
const resolved = resolveProviderModelDiscovery(name, provider);
|
|
383
400
|
const discovery = detachedFrozen({
|
|
384
401
|
...(resolved.spec ? { spec: resolved.spec } : {}),
|
|
@@ -413,6 +430,7 @@ export function captureProviderGather(
|
|
|
413
430
|
metadataModelIdCaseFold,
|
|
414
431
|
effectiveAlias,
|
|
415
432
|
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
|
|
433
|
+
...(refreshingOAuthAccountId ? { refreshingOAuthAccountId } : {}),
|
|
416
434
|
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
|
|
417
435
|
? { retainConfiguredModelIds }
|
|
418
436
|
: {}),
|
|
@@ -447,6 +465,7 @@ export function captureGatherFlight(
|
|
|
447
465
|
liveModels: provider.provider.liveModels ?? null,
|
|
448
466
|
credential: provider.provider.apiKey ?? null,
|
|
449
467
|
observedAuth: provider.observedAuth ?? null,
|
|
468
|
+
oauthAccount: provider.refreshingOAuthAccountId ?? null,
|
|
450
469
|
headers: provider.request.headersWithCredential,
|
|
451
470
|
url: provider.request.url,
|
|
452
471
|
}))),
|
|
@@ -16,7 +16,7 @@ import { getProviderRegistryEntry, providerCodexAccountMode } from "../../provid
|
|
|
16
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
17
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
18
18
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
19
|
-
import {
|
|
19
|
+
import { neutralizeIdentity } from "../../adapters/identity";
|
|
20
20
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
21
21
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
22
22
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
@@ -584,14 +584,14 @@ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
|
|
|
584
584
|
alias.display_name = presentation.displayName;
|
|
585
585
|
alias.description = presentation.description;
|
|
586
586
|
if (typeof alias.base_instructions === "string") {
|
|
587
|
-
alias.base_instructions =
|
|
587
|
+
alias.base_instructions = neutralizeIdentity(alias.base_instructions);
|
|
588
588
|
}
|
|
589
589
|
if (alias.model_messages && typeof alias.model_messages === "object" && !Array.isArray(alias.model_messages)) {
|
|
590
590
|
const modelMessages = alias.model_messages as Record<string, unknown>;
|
|
591
591
|
if (typeof modelMessages.instructions_template === "string") {
|
|
592
592
|
alias.model_messages = {
|
|
593
593
|
...modelMessages,
|
|
594
|
-
instructions_template:
|
|
594
|
+
instructions_template: neutralizeIdentity(modelMessages.instructions_template),
|
|
595
595
|
};
|
|
596
596
|
}
|
|
597
597
|
}
|
|
@@ -731,6 +731,7 @@ export function applyMultiAgentMode(
|
|
|
731
731
|
): RawEntry[] {
|
|
732
732
|
if (mode === "v2" && options.keepNativeChatGptOnV1 === true) {
|
|
733
733
|
for (const entry of entries) {
|
|
734
|
+
recordMultiAgentOrigin(entry);
|
|
734
735
|
entry.multi_agent_version = catalogEntryIsNativeChatGpt(entry) ? "v1" : "v2";
|
|
735
736
|
}
|
|
736
737
|
return entries;
|
|
@@ -739,6 +740,9 @@ export function applyMultiAgentMode(
|
|
|
739
740
|
// Restore upstream defaults: clear any stale forced multi_agent_version and
|
|
740
741
|
// re-apply upstream pins from the snapshot for native entries that have one.
|
|
741
742
|
for (const entry of entries) {
|
|
743
|
+
// A forced mode recorded what this row carried before it was overwritten; returning
|
|
744
|
+
// to default consumes that record whichever branch below decides the row.
|
|
745
|
+
const origin = takeMultiAgentOrigin(entry);
|
|
742
746
|
if (options.preserveDefaultMultiAgentVersion?.(entry)) continue;
|
|
743
747
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
744
748
|
const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
@@ -777,7 +781,17 @@ export function applyMultiAgentMode(
|
|
|
777
781
|
&& isNativeCatalogEntry
|
|
778
782
|
&& !hasNativeDefault
|
|
779
783
|
&& typeof entry.multi_agent_version === "string") {
|
|
780
|
-
|
|
784
|
+
// The baseline predates this native row, so it cannot say whether the live pin
|
|
785
|
+
// is genuine. With a recorded origin it no longer has to guess: restore what the
|
|
786
|
+
// row carried before the first forced mode (issue 5636). A row without a record
|
|
787
|
+
// (older catalogs) keeps the non-destructive read.
|
|
788
|
+
if (origin === undefined) continue;
|
|
789
|
+
if (typeof origin === "string") {
|
|
790
|
+
entry.multi_agent_version = origin;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (v2FeatureEnabled) entry.multi_agent_version = "v2";
|
|
794
|
+
else delete entry.multi_agent_version;
|
|
781
795
|
} else if (v2FeatureEnabled) {
|
|
782
796
|
entry.multi_agent_version = "v2";
|
|
783
797
|
} else {
|
|
@@ -787,11 +801,35 @@ export function applyMultiAgentMode(
|
|
|
787
801
|
return entries;
|
|
788
802
|
}
|
|
789
803
|
for (const entry of entries) {
|
|
804
|
+
recordMultiAgentOrigin(entry);
|
|
790
805
|
entry.multi_agent_version = mode;
|
|
791
806
|
}
|
|
792
807
|
return entries;
|
|
793
808
|
}
|
|
794
809
|
|
|
810
|
+
/**
|
|
811
|
+
* Provenance for a forced multi-agent stamp: the value the row carried before the first
|
|
812
|
+
* forced v1/v2 pass, or null when it carried none. Repeated forced passes never replace it,
|
|
813
|
+
* so a v1 -> v2 -> default round trip still restores the original. Codex ignores unknown
|
|
814
|
+
* catalog fields, as it does opencodex_catalog_kind.
|
|
815
|
+
*/
|
|
816
|
+
export const MULTI_AGENT_ORIGIN_FIELD = "opencodex_multi_agent_version_origin";
|
|
817
|
+
|
|
818
|
+
function recordMultiAgentOrigin(entry: RawEntry): void {
|
|
819
|
+
if (Object.hasOwn(entry, MULTI_AGENT_ORIGIN_FIELD)) return;
|
|
820
|
+
(entry as Record<string, unknown>)[MULTI_AGENT_ORIGIN_FIELD] = typeof entry.multi_agent_version === "string"
|
|
821
|
+
? entry.multi_agent_version
|
|
822
|
+
: null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Remove and return the recorded origin: a string pin, null for "no pin", undefined when unrecorded. */
|
|
826
|
+
function takeMultiAgentOrigin(entry: RawEntry): string | null | undefined {
|
|
827
|
+
if (!Object.hasOwn(entry, MULTI_AGENT_ORIGIN_FIELD)) return undefined;
|
|
828
|
+
const raw = (entry as Record<string, unknown>)[MULTI_AGENT_ORIGIN_FIELD];
|
|
829
|
+
delete (entry as Record<string, unknown>)[MULTI_AGENT_ORIGIN_FIELD];
|
|
830
|
+
return typeof raw === "string" ? raw : raw === null ? null : undefined;
|
|
831
|
+
}
|
|
832
|
+
|
|
795
833
|
export function normalizeRoutedCatalogEntry(
|
|
796
834
|
entry: RawEntry,
|
|
797
835
|
parallelToolCalls = false,
|
|
@@ -105,7 +105,7 @@ import type {
|
|
|
105
105
|
import type { CapturedProviderGather, CatalogGatherProviderAuthOutcome, CatalogGatherProviderModelOutcome, ModelsAuthResolution, ModelsAuthResolver } from "./gather-capture";
|
|
106
106
|
import { QUIET_AUTHORITATIVE_CATALOG_PROVIDERS, applyConfigHintsToCachedModels, applyProviderConfigHints, boundedOwnedBy, catalogHintsFromModelsApiItem, catalogHintsFromProviderConfig } from "./model-hints";
|
|
107
107
|
import { mergeConfiguredModelsIntoLiveCatalog, shouldExposeProviderModel, warnDroppedConfiguredIdsOnce } from "./model-visibility";
|
|
108
|
-
import { captureProviderGather, materializeCapturedHeaders } from "./gather-capture";
|
|
108
|
+
import { captureModelsRequest, captureProviderGather, materializeCapturedHeaders } from "./gather-capture";
|
|
109
109
|
|
|
110
110
|
export interface ProviderModelsResult {
|
|
111
111
|
readonly models: CatalogModel[];
|
|
@@ -151,7 +151,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
151
151
|
contextCap: number | undefined,
|
|
152
152
|
resolveAuth: ModelsAuthResolver,
|
|
153
153
|
): Promise<ProviderModelsResult> {
|
|
154
|
-
const { name, provider: prov, discovery,
|
|
154
|
+
const { name, provider: prov, discovery, metadataModelIdCaseFold } = captured;
|
|
155
155
|
const observed = (
|
|
156
156
|
models: CatalogModel[],
|
|
157
157
|
state: CatalogGatherProviderModelOutcome["state"],
|
|
@@ -217,10 +217,7 @@ export async function fetchProviderModelsWithAuth(
|
|
|
217
217
|
return observed(configured, "authoritative");
|
|
218
218
|
}
|
|
219
219
|
const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
220
|
-
? prov.authMode === "oauth"
|
|
221
|
-
effectiveGoogleMode(name, prov) === "cloud-code-assist"
|
|
222
|
-
|| prov.adapter === "devin"
|
|
223
|
-
)
|
|
220
|
+
? prov.authMode === "oauth"
|
|
224
221
|
? await getValidAccessTokenSnapshot(name)
|
|
225
222
|
.then(snapshot => ({
|
|
226
223
|
apiKey: snapshot.accessToken,
|
|
@@ -475,6 +472,11 @@ export async function fetchProviderModelsWithAuth(
|
|
|
475
472
|
"degraded",
|
|
476
473
|
);
|
|
477
474
|
}
|
|
475
|
+
// The captured request predates any refresh, so a refreshing gather rebuilds it
|
|
476
|
+
// from the auth it resolved: the token and its origin, together.
|
|
477
|
+
const request = resolveAuth.kind === "refreshing"
|
|
478
|
+
? captureModelsRequest(name, prov, auth.oauthApiBaseUrl)
|
|
479
|
+
: captured.request;
|
|
478
480
|
const url = request.url;
|
|
479
481
|
let headers = materializeCapturedHeaders(request, apiKey);
|
|
480
482
|
// One Ollama authority contract: for canonical ollama-cloud/ollama-native rows, discovery
|
|
@@ -649,11 +649,13 @@ export async function syncCatalogModels(
|
|
|
649
649
|
};
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
export
|
|
652
|
+
export type CodexCacheInvalidationOutcome = "written" | "unchanged" | "missing_catalog" | "desired_disabled" | "failed";
|
|
653
|
+
|
|
654
|
+
export function invalidateCodexModelsCacheWithPermitOutcome(
|
|
653
655
|
permit: CatalogWritePermit,
|
|
654
656
|
owningCodexHome: string,
|
|
655
657
|
options?: CodexCatalogSyncOptions,
|
|
656
|
-
):
|
|
658
|
+
): CodexCacheInvalidationOutcome {
|
|
657
659
|
try {
|
|
658
660
|
// This permit is a REACQUISITION: refreshCodexModelCatalog's commit released
|
|
659
661
|
// K before this rewrite runs, so the commit-path desired-state check cannot
|
|
@@ -661,9 +663,9 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
661
663
|
// routed cache write — re-read intent under this permit, same as the commit.
|
|
662
664
|
// The catalog-only sync override applies here too so an explicit refresh
|
|
663
665
|
// keeps the cache consistent with the catalog it just wrote.
|
|
664
|
-
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return
|
|
666
|
+
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return "desired_disabled";
|
|
665
667
|
const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
|
|
666
|
-
if (!existsSync(catalogPath)) return
|
|
668
|
+
if (!existsSync(catalogPath)) return "missing_catalog";
|
|
667
669
|
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
668
670
|
const models = catalog.models ?? catalog;
|
|
669
671
|
const cachePath = join(owningCodexHome, "models_cache.json");
|
|
@@ -711,14 +713,22 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
711
713
|
// `cacheSynced` mean what its name and its consumers already assume, and what
|
|
712
714
|
// `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
|
|
713
715
|
// already assert: a write happened.
|
|
714
|
-
if (!preparedBytesDifferFromDisk(preparedCache)) return
|
|
716
|
+
if (!preparedBytesDifferFromDisk(preparedCache)) return "unchanged";
|
|
715
717
|
replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
|
|
716
|
-
return
|
|
718
|
+
return "written";
|
|
717
719
|
} catch {
|
|
718
|
-
return
|
|
720
|
+
return "failed";
|
|
719
721
|
}
|
|
720
722
|
}
|
|
721
723
|
|
|
724
|
+
export function invalidateCodexModelsCacheWithPermit(
|
|
725
|
+
permit: CatalogWritePermit,
|
|
726
|
+
owningCodexHome: string,
|
|
727
|
+
options?: CodexCatalogSyncOptions,
|
|
728
|
+
): boolean {
|
|
729
|
+
return invalidateCodexModelsCacheWithPermitOutcome(permit, owningCodexHome, options) === "written";
|
|
730
|
+
}
|
|
731
|
+
|
|
722
732
|
export function invalidateCodexModelsCache(options?: CodexCatalogSyncOptions): boolean {
|
|
723
733
|
const owningCodexHome = getCodexHome();
|
|
724
734
|
const outcome = withCatalogWriteSerialization(
|
|
@@ -46,7 +46,7 @@ export {
|
|
|
46
46
|
export {
|
|
47
47
|
syncCatalogModels,
|
|
48
48
|
invalidateCodexModelsCache,
|
|
49
|
-
invalidateCodexModelsCacheWithPermit,
|
|
49
|
+
invalidateCodexModelsCacheWithPermit, invalidateCodexModelsCacheWithPermitOutcome,
|
|
50
50
|
} from "./retained-sync";
|
|
51
|
-
export type { CodexCatalogSyncOptions } from "./retained-sync";
|
|
51
|
+
export type { CodexCacheInvalidationOutcome, CodexCatalogSyncOptions } from "./retained-sync";
|
|
52
52
|
export { restoreCodexCatalog, restoreCodexCatalogWithPermit } from "./restore";
|
|
@@ -40,6 +40,7 @@ import { rootShells, type DesktopAppAdapter, type DesktopExec, type DesktopProce
|
|
|
40
40
|
import { darwinDesktopAppAdapter, darwinDefaultExec } from "./desktop-app/darwin";
|
|
41
41
|
import { linuxDesktopAppAdapter, linuxDefaultExec } from "./desktop-app/linux";
|
|
42
42
|
import { windowsDesktopAppAdapter, windowsDefaultExec } from "./desktop-app/windows";
|
|
43
|
+
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
43
44
|
|
|
44
45
|
export type { DesktopAppExecOptions } from "./desktop-app/types";
|
|
45
46
|
|
|
@@ -81,6 +82,7 @@ export interface DesktopAppRestartIo {
|
|
|
81
82
|
|
|
82
83
|
export type DesktopAppRestartReason =
|
|
83
84
|
| "unsupported_platform"
|
|
85
|
+
| "test_environment"
|
|
84
86
|
| "package_discovery_failed"
|
|
85
87
|
| "process_probe_failed"
|
|
86
88
|
| "no_targets"
|
|
@@ -213,6 +215,12 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
|
|
|
213
215
|
const adapter = io.adapter ?? selected?.adapter;
|
|
214
216
|
const exec = io.execFile ?? selected?.exec;
|
|
215
217
|
if (!adapter || !exec) return skipped("unsupported_platform");
|
|
218
|
+
// In an armed test process a call without an injected exec would reach the real OS (an injected
|
|
219
|
+
// adapter still execs through the platform default): on a developer Mac, `performCodexRestart`
|
|
220
|
+
// tests quit the user's ChatGPT (Codex) app and relaunched it through `/usr/bin/open` with the
|
|
221
|
+
// runner's sandbox HOME, logged out. Armed means the test preload's flag, not NODE_ENV, for the
|
|
222
|
+
// reason test-home-guard gives: a real `NODE_ENV=test ocx ...` must still restart the app.
|
|
223
|
+
if (!io.execFile && isTestHomeGuardArmed()) return skipped("test_environment");
|
|
216
224
|
|
|
217
225
|
// Step 0. Two restarts at once are destructive rather than merely wasteful: the
|
|
218
226
|
// first quits and relaunches, the second sees the freshly started shell as a target
|
|
@@ -88,7 +88,15 @@ export function describeCodexDesktopSwitches(
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Re-run the Codex config injection so a setting that lives in `~/.codex/config.toml` follows the
|
|
93
|
+
* stored config NOW rather than at the next `ocx sync`.
|
|
94
|
+
*
|
|
95
|
+
* Shared by the Desktop switches (`codexDesktopAuthless`, `codexClientCompaction`) and the
|
|
96
|
+
* web-search sidecar's Codex-side key. Both write through the same artifact transaction, so the
|
|
97
|
+
* failure vocabulary — and the "run 'ocx sync' to retry" advice that reads it — has to be one thing.
|
|
98
|
+
*/
|
|
99
|
+
export async function applyCodexConfigInjection(
|
|
92
100
|
config: OcxConfig,
|
|
93
101
|
): Promise<CodexDesktopSwitchApply> {
|
|
94
102
|
if (!shouldSyncCodexOnStart(config)) {
|
package/src/codex/home.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, posix, resolve, win32 } from "node:path";
|
|
4
|
-
import { expandUserPath } from "../config";
|
|
4
|
+
import { expandUserPath } from "../config/paths";
|
|
5
5
|
import { redactUserPath } from "../lib/redact";
|
|
6
6
|
|
|
7
7
|
export type CodexHomeDeps = {
|
|
@@ -135,9 +135,48 @@ export function findWslWindowsCodexHome(deps: CodexHomeDeps = {}): string | null
|
|
|
135
135
|
export function defaultCodexHome(deps: CodexHomeDeps = {}): string {
|
|
136
136
|
const home = (deps.homedir ?? homedir)();
|
|
137
137
|
const defaultHome = join(home, ".codex");
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// A local ~/.codex that Codex is already using is the user's Codex home even before
|
|
139
|
+
// config.toml exists (a fresh install: login writes auth.json, first use writes
|
|
140
|
+
// sessions/ and history.jsonl). A local directory with none of that state is not
|
|
141
|
+
// evidence of a local Codex: before #5441 such a home let WSL discovery pick the
|
|
142
|
+
// Windows home, and existing WSL users who run against that Windows home must not
|
|
143
|
+
// be moved to an empty local one on upgrade.
|
|
144
|
+
if (localCodexHomeIsDirectory(defaultHome, deps) && localCodexHomeInUse(defaultHome, deps)) return defaultHome;
|
|
145
|
+
return findWslWindowsCodexHome(deps) ?? defaultHome;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function localCodexHomeInUse(home: string, deps: CodexHomeDeps): boolean {
|
|
149
|
+
// Files and directories Codex itself writes into a home it is using. Kept local: defaultCodexHome
|
|
150
|
+
// runs during other modules' initialisation (the storage workers reach it through an import
|
|
151
|
+
// cycle), and a module-level const declared below it is still in its temporal dead zone then.
|
|
152
|
+
return ["config.toml", "auth.json", "sessions", "history.jsonl"].some(entry => pathPresent(join(home, entry), deps));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** stat-based presence: an unexpected stat error counts as present, never as a reason to switch homes. */
|
|
156
|
+
function pathPresent(path: string, deps: CodexHomeDeps): boolean {
|
|
157
|
+
const stat = deps.statSync ?? statSync;
|
|
158
|
+
try {
|
|
159
|
+
stat(path);
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
163
|
+
return !(code === "ENOENT" || code === "ENOTDIR");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function localCodexHomeIsDirectory(path: string, deps: CodexHomeDeps): boolean {
|
|
168
|
+
const stat = deps.statSync ?? statSync;
|
|
169
|
+
// stat, not existsSync: existsSync reports false for an access error too, and that
|
|
170
|
+
// must not read as "absent" and hand the user's state to a different home.
|
|
171
|
+
try {
|
|
172
|
+
return stat(path).isDirectory();
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const code = (error as NodeJS.ErrnoException | null)?.code;
|
|
175
|
+
if (code === "ENOENT" || code === "ENOTDIR") return false;
|
|
176
|
+
// An unreadable local home is still the local home; never switch to a
|
|
177
|
+
// different Codex home because a stat failed for an unexpected reason.
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
141
180
|
}
|
|
142
181
|
|
|
143
182
|
export function resolveCodexHomeDir(deps: CodexHomeDeps = {}): string {
|