@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
package/src/codex/inject.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import {
|
|
4
3
|
atomicWriteFile,
|
|
5
4
|
loadConfig,
|
|
6
5
|
observeConfigGeneration,
|
|
7
6
|
readConfigAdmissionSnapshot,
|
|
8
|
-
subagentDefaultSyncEffective,
|
|
9
7
|
websocketsEnabled,
|
|
10
8
|
withConfigMutationLockSync,
|
|
11
9
|
} from "../config";
|
|
@@ -30,7 +28,6 @@ import {
|
|
|
30
28
|
} from "./inject-coordination";
|
|
31
29
|
import { readIntegrationRecord } from "./integration-record";
|
|
32
30
|
import { classifyNativeRoutedResidue } from "./native-residue";
|
|
33
|
-
import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
34
31
|
import {
|
|
35
32
|
resolveCodexCoordinatorDatabasePath,
|
|
36
33
|
resolveEffectiveUserIdentity,
|
|
@@ -40,14 +37,10 @@ import {
|
|
|
40
37
|
markJournalInjectedState,
|
|
41
38
|
journaledInjectedOpenaiBaseUrl,
|
|
42
39
|
journaledInjectedRealtimeWsBaseUrl,
|
|
43
|
-
journaledInjectedCatalogPath,
|
|
44
40
|
removeJournal,
|
|
45
|
-
restoreJournalState,
|
|
46
41
|
writeJournal,
|
|
47
42
|
} from "./journal";
|
|
48
|
-
import {
|
|
49
|
-
import { restoreCodexCatalogWithPermit } from "./catalog/sync";
|
|
50
|
-
import { preflightCodexHistoryInjection, syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
|
|
43
|
+
import { preflightCodexHistoryInjection } from "./history-provider";
|
|
51
44
|
import {
|
|
52
45
|
describeHistoryJobFailure,
|
|
53
46
|
deriveCodexHistoryOperation,
|
|
@@ -56,36 +49,49 @@ import {
|
|
|
56
49
|
type CodexHistoryJobOutcome,
|
|
57
50
|
} from "./history-job";
|
|
58
51
|
import {
|
|
59
|
-
OCX_SECTION_MARKER,
|
|
60
52
|
REALTIME_WS_BASE_URL_KEY,
|
|
61
53
|
hasInjectedCodexRouting,
|
|
62
54
|
hasInjectedOpenaiBaseUrl,
|
|
63
|
-
isRootOpenaiBaseUrlLine,
|
|
64
|
-
isRootRealtimeWsBaseUrlLine,
|
|
65
|
-
providerTableStart,
|
|
66
|
-
providerTableString,
|
|
67
55
|
rootTomlString,
|
|
68
56
|
stripJournaledOpenaiBaseUrl,
|
|
69
|
-
tomlStringPattern,
|
|
70
57
|
} from "./injected-marker";
|
|
71
58
|
import {
|
|
72
59
|
CODEX_CONFIG_PATH,
|
|
73
60
|
CODEX_PROFILE_PATH,
|
|
74
|
-
DEFAULT_CATALOG_PATH,
|
|
75
61
|
getCodexHome,
|
|
76
|
-
parseTomlString,
|
|
77
|
-
readRootTomlString,
|
|
78
|
-
resolveCodexConfigPath,
|
|
79
62
|
resolveCodexStateDbPath,
|
|
80
63
|
tomlString,
|
|
81
64
|
} from "./paths";
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
84
|
-
transformManagedSubagentDefaults,
|
|
85
|
-
type ManagedSubagentDefaults,
|
|
86
|
-
} from "./subagent-defaults";
|
|
65
|
+
import { transformManagedSubagentDefaults } from "./subagent-defaults";
|
|
87
66
|
import type { OcxConfig } from "../types";
|
|
88
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
configuredManagedSubagentDefaults,
|
|
69
|
+
standaloneCodexRoutingTarget,
|
|
70
|
+
usesProviderTable,
|
|
71
|
+
validateCodexRoutingTarget,
|
|
72
|
+
type CodexRoutingTarget,
|
|
73
|
+
} from "./inject/routing-target";
|
|
74
|
+
import {
|
|
75
|
+
applyEol,
|
|
76
|
+
buildProfileFileForTarget,
|
|
77
|
+
buildProviderTableBlockForTarget,
|
|
78
|
+
chooseCatalogPathForInjection,
|
|
79
|
+
dominantEol,
|
|
80
|
+
ensureFastModeFeature,
|
|
81
|
+
externalCodexModelProvider,
|
|
82
|
+
normalizeServiceTier,
|
|
83
|
+
removeProfileSection,
|
|
84
|
+
setRootModelCatalogPath,
|
|
85
|
+
setRootModelProvider,
|
|
86
|
+
setRootOpenaiBaseUrlForTarget,
|
|
87
|
+
setRootRealtimeWsBaseUrl,
|
|
88
|
+
stripExistingModelProvider,
|
|
89
|
+
stripInjectedOpenaiBaseUrl,
|
|
90
|
+
stripOpencodexCatalogPath,
|
|
91
|
+
stripRootContextWindowOverrides,
|
|
92
|
+
} from "./inject/config-toml";
|
|
93
|
+
import { hasOcxProviderTable, removeOcxSection } from "./inject/remove";
|
|
94
|
+
|
|
89
95
|
|
|
90
96
|
export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
91
97
|
|
|
@@ -93,37 +99,6 @@ export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthH
|
|
|
93
99
|
// without importing this module back. Re-exported for existing external callers.
|
|
94
100
|
export { hasInjectedCodexRouting, hasInjectedOpenaiBaseUrl };
|
|
95
101
|
|
|
96
|
-
export function externalCodexModelProvider(content: string): string | null {
|
|
97
|
-
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
98
|
-
return provider && provider !== "openai" && provider !== "opencodex"
|
|
99
|
-
? provider
|
|
100
|
-
: null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function currentExternalCodexModelProvider(): string | null {
|
|
104
|
-
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
105
|
-
return externalCodexModelProvider(readFileSync(CODEX_CONFIG_PATH, "utf8"));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Detect the file's dominant line ending. Every transform in this module is LF-pure
|
|
110
|
-
* (split("\n") + hard "\n" joins), so CRLF configs (Windows-edited config.toml) are
|
|
111
|
-
* normalized to LF at the pipeline boundary and converted back on write — otherwise a
|
|
112
|
-
* single inject would leave a mixed-EOL file.
|
|
113
|
-
*/
|
|
114
|
-
export function dominantEol(content: string): "\r\n" | "\n" {
|
|
115
|
-
const crlf = (content.match(/\r\n/g) ?? []).length;
|
|
116
|
-
if (crlf === 0) return "\n";
|
|
117
|
-
const bareLf = (content.match(/\n/g) ?? []).length - crlf;
|
|
118
|
-
return crlf >= bareLf ? "\r\n" : "\n";
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Normalize all line endings to `eol` (CRLF first collapsed to LF, then expanded). */
|
|
122
|
-
export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
123
|
-
const lf = content.replace(/\r\n/g, "\n");
|
|
124
|
-
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
102
|
/**
|
|
128
103
|
* Design B (2026-07-06): loopback installs no longer re-tag the provider. Instead of
|
|
129
104
|
* `model_provider = "opencodex"` + a `[model_providers.opencodex]` table, we set the official
|
|
@@ -170,727 +145,6 @@ function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): vo
|
|
|
170
145
|
}
|
|
171
146
|
}
|
|
172
147
|
|
|
173
|
-
export interface CodexRoutingTarget {
|
|
174
|
-
baseUrl: string;
|
|
175
|
-
requiresAdmissionToken: boolean;
|
|
176
|
-
tokenEnv: "OPENCODEX_API_AUTH_TOKEN";
|
|
177
|
-
/**
|
|
178
|
-
* Opt-in authless Codex Desktop mode (#1107): inject the dedicated provider table with
|
|
179
|
-
* `requires_openai_auth = false` so Desktop skips the ChatGPT login gate. Only ever true for
|
|
180
|
-
* loopback targets that need no admission token; non-loopback admission is a separate layer
|
|
181
|
-
* and is never weakened by this flag.
|
|
182
|
-
*/
|
|
183
|
-
desktopAuthless?: boolean;
|
|
184
|
-
/** Select the dedicated provider identity so Codex owns compaction locally. */
|
|
185
|
-
clientCompaction?: boolean;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTarget {
|
|
189
|
-
let parsed: URL;
|
|
190
|
-
try {
|
|
191
|
-
parsed = new URL(target.baseUrl);
|
|
192
|
-
} catch {
|
|
193
|
-
throw new TypeError("Codex routing target must be an absolute HTTP(S) /v1 URL");
|
|
194
|
-
}
|
|
195
|
-
if (
|
|
196
|
-
(parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
197
|
-
|| parsed.username
|
|
198
|
-
|| parsed.password
|
|
199
|
-
|| parsed.pathname !== "/v1"
|
|
200
|
-
|| parsed.search
|
|
201
|
-
|| parsed.hash
|
|
202
|
-
|| target.tokenEnv !== "OPENCODEX_API_AUTH_TOKEN"
|
|
203
|
-
) {
|
|
204
|
-
throw new TypeError("Codex routing target must be a canonical HTTP(S) /v1 URL without credentials, query, or fragment");
|
|
205
|
-
}
|
|
206
|
-
return { ...target, baseUrl: `${parsed.origin}/v1` };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Provider-table form is used when auth, admission, or compaction policy needs a dedicated provider. */
|
|
210
|
-
function usesProviderTable(target: CodexRoutingTarget): boolean {
|
|
211
|
-
return target.requiresAdmissionToken
|
|
212
|
-
|| target.desktopAuthless === true
|
|
213
|
-
|| target.clientCompaction === true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function standaloneCodexRoutingTarget(
|
|
217
|
-
port: number,
|
|
218
|
-
config?: Pick<
|
|
219
|
-
OcxConfig,
|
|
220
|
-
"hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless" | "codexClientCompaction"
|
|
221
|
-
>,
|
|
222
|
-
): CodexRoutingTarget {
|
|
223
|
-
// An enabled listener with no `port` is the companion form: it answers on `port` itself,
|
|
224
|
-
// bound to 127.0.0.1 (#4236). Resolving it through the shared helper is what makes the
|
|
225
|
-
// one-port hub work without every writer repeating `?? port`.
|
|
226
|
-
const loopback = config?.unauthenticatedLoopbackListener;
|
|
227
|
-
const effectivePort = effectiveLoopbackListenerPort(config, port) ?? port;
|
|
228
|
-
const hostname = loopback?.enabled ? undefined : config?.hostname;
|
|
229
|
-
const requiresAdmissionToken = loopback?.enabled ? false : shouldInjectApiAuthHeader(config);
|
|
230
|
-
return {
|
|
231
|
-
baseUrl: `http://${providerBaseHost(hostname)}:${effectivePort}/v1`,
|
|
232
|
-
requiresAdmissionToken,
|
|
233
|
-
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
234
|
-
...(config?.codexDesktopAuthless === true && !requiresAdmissionToken
|
|
235
|
-
? { desktopAuthless: true }
|
|
236
|
-
: {}),
|
|
237
|
-
...(config?.codexClientCompaction === true && !requiresAdmissionToken
|
|
238
|
-
? { clientCompaction: true }
|
|
239
|
-
: {}),
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function routingTargetOrigin(target: CodexRoutingTarget): string {
|
|
244
|
-
return target.baseUrl.slice(0, -3);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function configuredManagedSubagentDefaults(
|
|
248
|
-
config:
|
|
249
|
-
| Pick<
|
|
250
|
-
OcxConfig,
|
|
251
|
-
"injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"
|
|
252
|
-
>
|
|
253
|
-
| undefined,
|
|
254
|
-
): ManagedSubagentDefaults | null {
|
|
255
|
-
if (!subagentDefaultSyncEffective(config ?? {})) return null;
|
|
256
|
-
return {
|
|
257
|
-
model: config!.injectionModel!.trim(),
|
|
258
|
-
...(config!.injectionEffort?.trim()
|
|
259
|
-
? { reasoningEffort: config!.injectionEffort.trim() }
|
|
260
|
-
: {}),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* The `[model_providers.opencodex]` TABLE only. A table is position-independent in TOML, so it is
|
|
266
|
-
* safe to append at EOF. The bare root key `model_provider = "opencodex"` is NOT included here —
|
|
267
|
-
* it must live at the document root (before any table header) and is set separately by
|
|
268
|
-
* setRootModelProvider(). Appending the bare key at EOF was the original bug: it nested under
|
|
269
|
-
* whatever `[table]` happened to be open last (e.g. `[plugins."chrome@openai-bundled"]`), so Codex
|
|
270
|
-
* never saw a global model_provider and silently fell back to the `openai` (ChatGPT) provider.
|
|
271
|
-
*/
|
|
272
|
-
export function providerBaseHost(hostname: string | undefined): string {
|
|
273
|
-
const trimmed = (hostname ?? "127.0.0.1").trim();
|
|
274
|
-
const lower = trimmed.toLowerCase();
|
|
275
|
-
// Match what the server actually binds. Writing "localhost" while binding IPv4-only
|
|
276
|
-
// 127.0.0.1 breaks on Windows, where localhost commonly resolves to ::1 first.
|
|
277
|
-
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
278
|
-
if (
|
|
279
|
-
isLoopbackHostname(trimmed) ||
|
|
280
|
-
trimmed === "0.0.0.0" ||
|
|
281
|
-
trimmed === "::" ||
|
|
282
|
-
trimmed === "[::]"
|
|
283
|
-
)
|
|
284
|
-
return "127.0.0.1";
|
|
285
|
-
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
286
|
-
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function buildProviderTableBlock(
|
|
290
|
-
port: number,
|
|
291
|
-
supportsWebsockets?: boolean,
|
|
292
|
-
includeApiAuthHeader?: boolean,
|
|
293
|
-
hostname?: string,
|
|
294
|
-
): string;
|
|
295
|
-
export function buildProviderTableBlock(
|
|
296
|
-
target: CodexRoutingTarget,
|
|
297
|
-
supportsWebsockets?: boolean,
|
|
298
|
-
): string;
|
|
299
|
-
export function buildProviderTableBlock(
|
|
300
|
-
portOrTarget: number | CodexRoutingTarget,
|
|
301
|
-
supportsWebsockets = false,
|
|
302
|
-
includeApiAuthHeader = false,
|
|
303
|
-
hostname?: string,
|
|
304
|
-
): string {
|
|
305
|
-
const target = typeof portOrTarget === "number"
|
|
306
|
-
? validateCodexRoutingTarget({
|
|
307
|
-
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
308
|
-
requiresAdmissionToken: includeApiAuthHeader,
|
|
309
|
-
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
310
|
-
})
|
|
311
|
-
: validateCodexRoutingTarget(portOrTarget);
|
|
312
|
-
return buildProviderTableBlockForTarget(target, supportsWebsockets);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function buildProviderTableBlockForTarget(
|
|
316
|
-
target: CodexRoutingTarget,
|
|
317
|
-
supportsWebsockets = false,
|
|
318
|
-
): string {
|
|
319
|
-
const lines = [
|
|
320
|
-
"",
|
|
321
|
-
OCX_SECTION_MARKER,
|
|
322
|
-
"[model_providers.opencodex]",
|
|
323
|
-
'name = "OpenCodex Proxy"',
|
|
324
|
-
`base_url = ${tomlString(target.baseUrl)}`,
|
|
325
|
-
'wire_api = "responses"',
|
|
326
|
-
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
327
|
-
`requires_openai_auth = ${target.desktopAuthless === true ? "false" : "true"}`,
|
|
328
|
-
];
|
|
329
|
-
if (target.requiresAdmissionToken) {
|
|
330
|
-
// codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and
|
|
331
|
-
// hard-errors on a missing/empty variable instead of silently omitting auth. It
|
|
332
|
-
// coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the
|
|
333
|
-
// login/account UX), and the server substitutes stored main auth for our admission
|
|
334
|
-
// bearer (#1686), so the modern form is strictly better than the legacy
|
|
335
|
-
// env_http_headers table this line used to emit.
|
|
336
|
-
lines.push(`env_key = ${tomlString(target.tokenEnv)}`);
|
|
337
|
-
}
|
|
338
|
-
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
339
|
-
return lines.join("\n") + "\n";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export function buildOpenaiBaseUrlLine(
|
|
343
|
-
port: number,
|
|
344
|
-
hostname?: string,
|
|
345
|
-
): string;
|
|
346
|
-
export function buildOpenaiBaseUrlLine(target: CodexRoutingTarget): string;
|
|
347
|
-
export function buildOpenaiBaseUrlLine(
|
|
348
|
-
portOrTarget: number | CodexRoutingTarget,
|
|
349
|
-
hostname?: string,
|
|
350
|
-
): string {
|
|
351
|
-
return typeof portOrTarget === "number"
|
|
352
|
-
? `openai_base_url = "http://${providerBaseHost(hostname)}:${portOrTarget}/v1"`
|
|
353
|
-
: buildOpenaiBaseUrlLineForTarget(validateCodexRoutingTarget(portOrTarget));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function buildOpenaiBaseUrlLineForTarget(target: CodexRoutingTarget): string {
|
|
357
|
-
return `openai_base_url = ${tomlString(target.baseUrl)}`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Realtime sideband override (codex-rs `experimental_realtime_ws_base_url`), written with the
|
|
362
|
-
* SAME value as `openai_base_url`. Desktop voice creates its WebRTC call through the proxy
|
|
363
|
-
* (`POST /v1/live`, answered under the Pool account the proxy selects) but, since openai/codex
|
|
364
|
-
* 438c9e98d (#35830), joins the sideband at `wss://api.openai.com/v1/live/{callId}` with the
|
|
365
|
-
* app's own login unless this key redirects it. Two accounts, one call: the join 404s. Pointing
|
|
366
|
-
* the key at the proxy sends the join through `GET /v1/live/{callId}` (src/server/live.ts),
|
|
367
|
-
* where the same Pool account is reused. codex-rs turns `http` into `ws` and appends
|
|
368
|
-
* `/live/{callId}` itself; the value must stay the canonical `/v1` root.
|
|
369
|
-
*/
|
|
370
|
-
export function buildRealtimeWsBaseUrlLine(target: CodexRoutingTarget): string {
|
|
371
|
-
return `${REALTIME_WS_BASE_URL_KEY} = ${tomlString(target.baseUrl)}`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Design B root-key injection: place `OCX_SECTION_MARKER` + `openai_base_url` at the document
|
|
376
|
-
* ROOT (before the first table header). Idempotent: an existing marker-owned line is rewritten
|
|
377
|
-
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
378
|
-
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
379
|
-
*/
|
|
380
|
-
export function setRootOpenaiBaseUrl(
|
|
381
|
-
content: string,
|
|
382
|
-
port: number,
|
|
383
|
-
hostname?: string,
|
|
384
|
-
): { content: string; keptUserBaseUrl: boolean };
|
|
385
|
-
export function setRootOpenaiBaseUrl(
|
|
386
|
-
content: string,
|
|
387
|
-
target: CodexRoutingTarget,
|
|
388
|
-
): { content: string; keptUserBaseUrl: boolean };
|
|
389
|
-
export function setRootOpenaiBaseUrl(
|
|
390
|
-
content: string,
|
|
391
|
-
portOrTarget: number | CodexRoutingTarget,
|
|
392
|
-
hostname?: string,
|
|
393
|
-
): { content: string; keptUserBaseUrl: boolean } {
|
|
394
|
-
if (typeof portOrTarget !== "number") {
|
|
395
|
-
return setRootOpenaiBaseUrlForTarget(content, validateCodexRoutingTarget(portOrTarget));
|
|
396
|
-
}
|
|
397
|
-
const lines = content.split("\n");
|
|
398
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
399
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
400
|
-
const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLine(portOrTarget, hostname));
|
|
401
|
-
|
|
402
|
-
for (let i = 0; i < rootEnd; i++) {
|
|
403
|
-
if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
|
|
404
|
-
const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
|
|
405
|
-
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
406
|
-
lines[i] = key;
|
|
407
|
-
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (firstTable === -1) {
|
|
411
|
-
return {
|
|
412
|
-
content:
|
|
413
|
-
content.replace(/\n+$/, "") +
|
|
414
|
-
"\n" +
|
|
415
|
-
OCX_SECTION_MARKER +
|
|
416
|
-
"\n" +
|
|
417
|
-
key +
|
|
418
|
-
"\n",
|
|
419
|
-
keptUserBaseUrl: false,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
let insertAt = firstTable;
|
|
423
|
-
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
424
|
-
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
425
|
-
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function setRootOpenaiBaseUrlForTarget(
|
|
429
|
-
content: string,
|
|
430
|
-
target: CodexRoutingTarget,
|
|
431
|
-
): { content: string; keptUserBaseUrl: boolean } {
|
|
432
|
-
const lines = content.split("\n");
|
|
433
|
-
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
434
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
435
|
-
const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLineForTarget(target));
|
|
436
|
-
for (let index = 0; index < rootEnd; index += 1) {
|
|
437
|
-
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
438
|
-
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
439
|
-
if (!markerOwned) return { content, keptUserBaseUrl: true };
|
|
440
|
-
lines[index] = key;
|
|
441
|
-
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
442
|
-
}
|
|
443
|
-
if (firstTable === -1) {
|
|
444
|
-
return {
|
|
445
|
-
content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
|
|
446
|
-
keptUserBaseUrl: false,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
let insertAt = firstTable;
|
|
450
|
-
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
|
|
451
|
-
lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
|
|
452
|
-
return { content: lines.join("\n"), keptUserBaseUrl: false };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Companion to `setRootOpenaiBaseUrlForTarget` for the realtime sideband override. Same
|
|
457
|
-
* ownership rule, applied per key: the line is ours only when the marker sits directly
|
|
458
|
-
* above it; a user's own line (no marker above it) is kept and nothing is injected. The
|
|
459
|
-
* key gets its OWN marker line rather than sharing the routing override's, so a user line
|
|
460
|
-
* that happens to sit right under our `openai_base_url` is never mistaken for ours.
|
|
461
|
-
* Placement: directly after the marker-owned `openai_base_url` pair. Only ever called on
|
|
462
|
-
* the Design B (loopback) path right after the routing override was written — the legacy
|
|
463
|
-
* provider-table form needs the admission-token header, which the sideband cannot carry.
|
|
464
|
-
*/
|
|
465
|
-
export function setRootRealtimeWsBaseUrl(
|
|
466
|
-
content: string,
|
|
467
|
-
target: CodexRoutingTarget,
|
|
468
|
-
): { content: string; keptUserRealtimeWsBaseUrl: boolean } {
|
|
469
|
-
const lines = content.split("\n");
|
|
470
|
-
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
471
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
472
|
-
const key = buildRealtimeWsBaseUrlLine(validateCodexRoutingTarget(target));
|
|
473
|
-
for (let index = 0; index < rootEnd; index += 1) {
|
|
474
|
-
if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
|
|
475
|
-
const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
|
|
476
|
-
if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
|
|
477
|
-
lines[index] = key;
|
|
478
|
-
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
479
|
-
}
|
|
480
|
-
for (let index = 0; index < rootEnd; index += 1) {
|
|
481
|
-
if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
|
|
482
|
-
if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
|
|
483
|
-
lines.splice(index + 1, 0, OCX_SECTION_MARKER, key);
|
|
484
|
-
return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
|
|
485
|
-
}
|
|
486
|
-
// No marker-owned routing override to attach to: the override has no owner, so inject nothing.
|
|
487
|
-
return { content, keptUserRealtimeWsBaseUrl: false };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Remove the marker-owned root `openai_base_url` (marker line + the key line right after it).
|
|
492
|
-
* A user's own root override (no marker) survives; an orphaned marker with no key line after
|
|
493
|
-
* it is dropped too so repeated strip/inject cycles cannot accumulate marker comments.
|
|
494
|
-
* A marker-owned `experimental_realtime_ws_base_url` pair is removed by the same rule.
|
|
495
|
-
*/
|
|
496
|
-
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
497
|
-
const lines = content.split("\n");
|
|
498
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
499
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
500
|
-
const drop = new Set<number>();
|
|
501
|
-
for (let i = 0; i < rootEnd; i++) {
|
|
502
|
-
if (!lines[i].includes(OCX_SECTION_MARKER)) continue;
|
|
503
|
-
if (i + 1 < rootEnd && (isRootOpenaiBaseUrlLine(lines[i + 1]) || isRootRealtimeWsBaseUrlLine(lines[i + 1]))) {
|
|
504
|
-
drop.add(i);
|
|
505
|
-
drop.add(i + 1);
|
|
506
|
-
} else if (i + 1 >= rootEnd || lines[i + 1].trim() === "") {
|
|
507
|
-
drop.add(i); // orphaned marker at root
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (drop.size === 0) return content;
|
|
511
|
-
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export type CodexRoutingKind =
|
|
515
|
-
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
516
|
-
|
|
517
|
-
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
518
|
-
|
|
519
|
-
function ipv4Octets(hostname: string): number[] | null {
|
|
520
|
-
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
521
|
-
if (dotted) {
|
|
522
|
-
const octets = dotted.slice(1).map(Number);
|
|
523
|
-
return octets.some((octet) => octet > 255) ? null : octets;
|
|
524
|
-
}
|
|
525
|
-
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
526
|
-
if (!mapped) return null;
|
|
527
|
-
const high = Number.parseInt(mapped[1], 16);
|
|
528
|
-
const low = Number.parseInt(mapped[2], 16);
|
|
529
|
-
return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
533
|
-
try {
|
|
534
|
-
const url = new URL(value);
|
|
535
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
536
|
-
const hostname = url.hostname
|
|
537
|
-
.toLowerCase()
|
|
538
|
-
.replace(/^\[|\]$/g, "")
|
|
539
|
-
.replace(/\.$/, "");
|
|
540
|
-
if (!hostname) return "unknown";
|
|
541
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
542
|
-
return "local";
|
|
543
|
-
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
|
|
544
|
-
return "local";
|
|
545
|
-
const octets = ipv4Octets(hostname);
|
|
546
|
-
if (octets) {
|
|
547
|
-
if (octets.every((octet) => octet === 0)) return "local";
|
|
548
|
-
if (octets[0] === 127) return "local";
|
|
549
|
-
return "remote";
|
|
550
|
-
}
|
|
551
|
-
if (/^::ffff:/i.test(hostname)) return "unknown";
|
|
552
|
-
return "remote";
|
|
553
|
-
} catch {
|
|
554
|
-
return "unknown";
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/** Classify actual routing dependency separately from opencodex ownership. */
|
|
559
|
-
export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
560
|
-
const rootBaseUrl = rootTomlString(content, "openai_base_url");
|
|
561
|
-
if (rootBaseUrl) {
|
|
562
|
-
const endpoint = classifyRoutingEndpoint(rootBaseUrl);
|
|
563
|
-
if (endpoint === "unknown") return "unknown";
|
|
564
|
-
if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
|
|
565
|
-
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
566
|
-
}
|
|
567
|
-
const rootProvider = rootTomlString(content, "model_provider");
|
|
568
|
-
if (rootProvider) {
|
|
569
|
-
const providerTableExists =
|
|
570
|
-
providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
571
|
-
const providerBaseUrl = providerTableString(
|
|
572
|
-
content,
|
|
573
|
-
rootProvider,
|
|
574
|
-
"base_url",
|
|
575
|
-
);
|
|
576
|
-
if (providerBaseUrl) {
|
|
577
|
-
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
578
|
-
if (endpoint === "unknown") return "unknown";
|
|
579
|
-
if (rootProvider === "opencodex") return "opencodex-local";
|
|
580
|
-
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
581
|
-
}
|
|
582
|
-
if (
|
|
583
|
-
rootProvider === "opencodex" ||
|
|
584
|
-
providerTableExists ||
|
|
585
|
-
rootProvider !== "openai"
|
|
586
|
-
)
|
|
587
|
-
return "unknown";
|
|
588
|
-
}
|
|
589
|
-
return "native";
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/** Read-only probe used by status, doctor, and the dashboard. */
|
|
593
|
-
export function isCodexRoutingInjected(): boolean {
|
|
594
|
-
const path = CODEX_CONFIG_PATH;
|
|
595
|
-
if (!existsSync(path)) return false;
|
|
596
|
-
try {
|
|
597
|
-
return hasInjectedCodexRouting(readFileSync(path, "utf8"));
|
|
598
|
-
} catch {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function getCodexRoutingKind(): CodexRoutingKind {
|
|
604
|
-
const path = CODEX_CONFIG_PATH;
|
|
605
|
-
if (!existsSync(path)) return "native";
|
|
606
|
-
try {
|
|
607
|
-
return classifyCodexRouting(readFileSync(path, "utf8"));
|
|
608
|
-
} catch {
|
|
609
|
-
return "unknown";
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Strip every existing `model_provider` line that we must not duplicate: any line set to
|
|
615
|
-
* "opencodex" (wherever it sits — including a previously mis-nested one under a table), plus any
|
|
616
|
-
* ROOT-level model_provider (before the first table) of any value, since we override the global.
|
|
617
|
-
* A `model_provider` legitimately inside a user table/profile with a non-opencodex value is left
|
|
618
|
-
* untouched.
|
|
619
|
-
*/
|
|
620
|
-
function stripExistingModelProvider(content: string): string {
|
|
621
|
-
const lines = content.split("\n");
|
|
622
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
623
|
-
const out: string[] = [];
|
|
624
|
-
lines.forEach((line, i) => {
|
|
625
|
-
if (/^\s*model_provider\s*=/.test(line)) {
|
|
626
|
-
const isOurs = /^\s*model_provider\s*=\s*"opencodex"\s*$/.test(line);
|
|
627
|
-
const isRoot = firstTable === -1 || i < firstTable;
|
|
628
|
-
if (isOurs || isRoot) return; // drop it
|
|
629
|
-
}
|
|
630
|
-
out.push(line);
|
|
631
|
-
});
|
|
632
|
-
return out.join("\n");
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Drop ROOT-level `model_context_window` overrides (keys before the first table header). Codex
|
|
637
|
-
* treats this root key as a global override that wins over the per-model catalog values, so a stale
|
|
638
|
-
* `model_context_window = 1000000` makes every model (e.g. gpt-5.5) report a 1M window. User-owned
|
|
639
|
-
* compaction limits do not alter the advertised context window and must survive reinjection.
|
|
640
|
-
*/
|
|
641
|
-
export function stripRootContextWindowOverrides(content: string): string {
|
|
642
|
-
const lines = content.split("\n");
|
|
643
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
644
|
-
return lines
|
|
645
|
-
.filter((line, i) => {
|
|
646
|
-
const isRoot = firstTable === -1 || i < firstTable;
|
|
647
|
-
return !isRoot || !/^\s*model_context_window\s*=/.test(line);
|
|
648
|
-
})
|
|
649
|
-
.join("\n");
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function stripRootRoutedModel(content: string): string {
|
|
653
|
-
const lines = content.split("\n");
|
|
654
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
655
|
-
return lines
|
|
656
|
-
.filter((line, i) => {
|
|
657
|
-
const isRoot = firstTable === -1 || i < firstTable;
|
|
658
|
-
if (!isRoot) return true;
|
|
659
|
-
const m = line.match(/^\s*model\s*=\s*("(?:\\.|[^"\\])*"|'[^']*')\s*$/);
|
|
660
|
-
if (!m) return true;
|
|
661
|
-
const model = parseTomlString(m[1]);
|
|
662
|
-
return !model?.includes("/");
|
|
663
|
-
})
|
|
664
|
-
.join("\n");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Insert `model_provider = "opencodex"` at the document ROOT — immediately before the first table
|
|
669
|
-
* header (TOML root keys must precede all tables). If there are no tables, append it to the root body.
|
|
670
|
-
*/
|
|
671
|
-
function setRootModelProvider(content: string): string {
|
|
672
|
-
const lines = content.split("\n");
|
|
673
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
674
|
-
const key = 'model_provider = "opencodex"';
|
|
675
|
-
if (firstTable === -1) {
|
|
676
|
-
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
677
|
-
}
|
|
678
|
-
let insertAt = firstTable;
|
|
679
|
-
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
680
|
-
lines.splice(insertAt, 0, key);
|
|
681
|
-
return lines.join("\n");
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function readRootModelCatalogPath(content: string): string | null {
|
|
685
|
-
const lines = content.split("\n");
|
|
686
|
-
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
687
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
688
|
-
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
689
|
-
let ownedCatalogPath: string | null = null;
|
|
690
|
-
for (let index = 0; index < rootEnd; index += 1) {
|
|
691
|
-
const match = modelCatalogAssignment.exec(lines[index]);
|
|
692
|
-
if (!match) continue;
|
|
693
|
-
const catalogPath = parseTomlString(match[1]);
|
|
694
|
-
if (!isOpencodexCatalogPath(catalogPath)) return catalogPath;
|
|
695
|
-
ownedCatalogPath ??= catalogPath;
|
|
696
|
-
}
|
|
697
|
-
return ownedCatalogPath;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function setRootModelCatalogPath(content: string, catalogPath: string): string {
|
|
701
|
-
const lines = content.split("\n");
|
|
702
|
-
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
703
|
-
const key = `model_catalog_json = ${tomlString(catalogPath)}`;
|
|
704
|
-
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
705
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
706
|
-
const ownedAssignments: number[] = [];
|
|
707
|
-
let hasUserAssignment = false;
|
|
708
|
-
for (let i = 0; i < rootEnd; i++) {
|
|
709
|
-
const m = modelCatalogAssignment.exec(lines[i]);
|
|
710
|
-
if (!m) continue;
|
|
711
|
-
const existing = parseTomlString(m[1]);
|
|
712
|
-
if (isOpencodexCatalogPath(existing)) {
|
|
713
|
-
ownedAssignments.push(i);
|
|
714
|
-
} else {
|
|
715
|
-
hasUserAssignment = true;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (hasUserAssignment) {
|
|
719
|
-
const owned = new Set(ownedAssignments);
|
|
720
|
-
return lines.filter((_, index) => !owned.has(index)).join("\n");
|
|
721
|
-
}
|
|
722
|
-
if (ownedAssignments.length > 0) {
|
|
723
|
-
lines[ownedAssignments[0]] = key;
|
|
724
|
-
const duplicates = new Set(ownedAssignments.slice(1));
|
|
725
|
-
return lines.filter((_, index) => !duplicates.has(index)).join("\n");
|
|
726
|
-
}
|
|
727
|
-
if (firstTable === -1) {
|
|
728
|
-
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
729
|
-
}
|
|
730
|
-
let insertAt = firstTable;
|
|
731
|
-
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
732
|
-
lines.splice(insertAt, 0, key);
|
|
733
|
-
return lines.join("\n");
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function removeProfileSection(content: string): string {
|
|
737
|
-
const lines = content.split("\n");
|
|
738
|
-
const filtered: string[] = [];
|
|
739
|
-
let inProfile = false;
|
|
740
|
-
for (const line of lines) {
|
|
741
|
-
if (line.trim() === "[profiles.opencodex]") {
|
|
742
|
-
inProfile = true;
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
if (inProfile) {
|
|
746
|
-
if (/^\s*\[/.test(line) && line.trim() !== "[profiles.opencodex]") {
|
|
747
|
-
inProfile = false;
|
|
748
|
-
filtered.push(line);
|
|
749
|
-
}
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
filtered.push(line);
|
|
753
|
-
}
|
|
754
|
-
return (
|
|
755
|
-
filtered
|
|
756
|
-
.join("\n")
|
|
757
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
758
|
-
.trimEnd() + "\n"
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function normalizeServiceTier(content: string): string {
|
|
763
|
-
return content.replace(
|
|
764
|
-
/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
|
|
765
|
-
'$1"fast"',
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function ensureFastModeFeature(content: string, fastMode?: boolean): string {
|
|
770
|
-
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
|
|
771
|
-
// false forces `fast_mode = false`, and undefined leaves the user's config
|
|
772
|
-
// untouched (no [features] table is added and an existing fast_mode line is
|
|
773
|
-
// preserved as-is). Table and key matching accept the valid TOML spellings
|
|
774
|
-
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
|
|
775
|
-
const lines = content.split("\n");
|
|
776
|
-
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
|
|
777
|
-
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
|
|
778
|
-
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
|
|
779
|
-
if (featuresStart === -1) {
|
|
780
|
-
if (fastMode === undefined) return content;
|
|
781
|
-
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const nextTable = lines.findIndex(
|
|
785
|
-
(line, index) => index > featuresStart && /^\s*\[/.test(line),
|
|
786
|
-
);
|
|
787
|
-
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
|
|
788
|
-
for (let i = featuresStart + 1; i < featuresEnd; i++) {
|
|
789
|
-
if (fastModeKey.test(lines[i])) {
|
|
790
|
-
if (fastMode === undefined) return lines.join("\n");
|
|
791
|
-
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
|
|
792
|
-
return lines.join("\n");
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (fastMode === undefined) return lines.join("\n");
|
|
797
|
-
let insertAt = featuresEnd;
|
|
798
|
-
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
799
|
-
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
800
|
-
return lines.join("\n");
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function isOpencodexCatalogPath(path: string): boolean {
|
|
804
|
-
return path.replace(/\\/g, "/").split("/").pop() === "opencodex-catalog.json";
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function stripOpencodexCatalogPath(content: string): string {
|
|
808
|
-
const modelCatalogAssignment = tomlStringPattern("model_catalog_json");
|
|
809
|
-
const lines = content.split("\n");
|
|
810
|
-
const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
811
|
-
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
812
|
-
return lines
|
|
813
|
-
.filter((line, index) => {
|
|
814
|
-
if (index >= rootEnd) return true;
|
|
815
|
-
const m = modelCatalogAssignment.exec(line);
|
|
816
|
-
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
817
|
-
})
|
|
818
|
-
.join("\n");
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets?: boolean, includeApiAuthHeader?: boolean, hostname?: string, fastMode?: boolean): string;
|
|
822
|
-
export function buildProfileFile(target: CodexRoutingTarget, catalogPath?: string | null, supportsWebsockets?: boolean, fastMode?: boolean): string;
|
|
823
|
-
export function buildProfileFile(
|
|
824
|
-
portOrTarget: number | CodexRoutingTarget,
|
|
825
|
-
catalogPath?: string | null,
|
|
826
|
-
supportsWebsockets = false,
|
|
827
|
-
includeApiAuthHeaderOrFastMode?: boolean,
|
|
828
|
-
hostname?: string,
|
|
829
|
-
fastMode?: boolean,
|
|
830
|
-
): string {
|
|
831
|
-
const target = typeof portOrTarget === "number"
|
|
832
|
-
? validateCodexRoutingTarget({
|
|
833
|
-
baseUrl: `http://${providerBaseHost(hostname)}:${portOrTarget}/v1`,
|
|
834
|
-
requiresAdmissionToken: includeApiAuthHeaderOrFastMode === true,
|
|
835
|
-
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
|
|
836
|
-
})
|
|
837
|
-
: validateCodexRoutingTarget(portOrTarget);
|
|
838
|
-
return buildProfileFileForTarget(
|
|
839
|
-
target,
|
|
840
|
-
catalogPath,
|
|
841
|
-
supportsWebsockets,
|
|
842
|
-
typeof portOrTarget === "number" ? fastMode : includeApiAuthHeaderOrFastMode,
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function buildProfileFileForTarget(
|
|
847
|
-
target: CodexRoutingTarget,
|
|
848
|
-
catalogPath?: string | null,
|
|
849
|
-
supportsWebsockets = false,
|
|
850
|
-
fastMode?: boolean,
|
|
851
|
-
): string {
|
|
852
|
-
const origin = routingTargetOrigin(target);
|
|
853
|
-
const host = new URL(origin).host;
|
|
854
|
-
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
855
|
-
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
856
|
-
// the x-opencodex-api-key env header); explicit Desktop policies share that shape.
|
|
857
|
-
if (!usesProviderTable(target)) {
|
|
858
|
-
const lines = [
|
|
859
|
-
"# OpenCodex proxy fallback config (Design B)",
|
|
860
|
-
`# Root override that points Codex's built-in openai provider at the proxy on ${host}.`,
|
|
861
|
-
"# Merge these root keys into ~/.codex/config.toml manually if auto-injection was removed.",
|
|
862
|
-
buildOpenaiBaseUrlLineForTarget(target),
|
|
863
|
-
];
|
|
864
|
-
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
865
|
-
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
866
|
-
return lines.join("\n");
|
|
867
|
-
}
|
|
868
|
-
const lines = [
|
|
869
|
-
"# OpenCodex proxy profile — use with: codex --profile opencodex",
|
|
870
|
-
`# Routes all model requests through the opencodex proxy at ${host}`,
|
|
871
|
-
'model_provider = "opencodex"',
|
|
872
|
-
];
|
|
873
|
-
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
874
|
-
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
875
|
-
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
876
|
-
return lines.join("\n");
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
export function chooseCatalogPathForInjection(
|
|
880
|
-
content: string,
|
|
881
|
-
requested?: string | null,
|
|
882
|
-
): string | null {
|
|
883
|
-
if (requested !== undefined) return requested;
|
|
884
|
-
|
|
885
|
-
const existing = readRootModelCatalogPath(content);
|
|
886
|
-
if (existing) {
|
|
887
|
-
const resolved = resolveCodexConfigPath(existing);
|
|
888
|
-
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
889
|
-
return existing;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
893
|
-
}
|
|
894
148
|
|
|
895
149
|
export interface CodexInjectResult {
|
|
896
150
|
success: boolean;
|
|
@@ -916,19 +170,10 @@ export interface CodexInjectResult {
|
|
|
916
170
|
const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
|
|
917
171
|
|
|
918
172
|
class CodexHistoryPreflightRefusal extends Error {}
|
|
919
|
-
class CodexRestoreRefusal extends Error {
|
|
920
|
-
constructor(readonly config: CodexRestoreConfigResult) {
|
|
921
|
-
super(config.message);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
173
|
let historyArtifactStageForTests: ((stage: string) => void) | undefined;
|
|
925
174
|
export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
|
|
926
175
|
historyArtifactStageForTests = hook;
|
|
927
176
|
}
|
|
928
|
-
let beforeRestoreConfigForTests: ((kind: string) => void) | undefined;
|
|
929
|
-
export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigForTests): void {
|
|
930
|
-
beforeRestoreConfigForTests = hook;
|
|
931
|
-
}
|
|
932
177
|
let beforeHistoryArtifactCommitForTests: ((kind: string) => void) | undefined;
|
|
933
178
|
export function setBeforeHistoryArtifactCommitForTests(hook: typeof beforeHistoryArtifactCommitForTests): void {
|
|
934
179
|
beforeHistoryArtifactCommitForTests = hook;
|
|
@@ -1661,656 +906,6 @@ async function injectCodexConfigImpl(
|
|
|
1661
906
|
};
|
|
1662
907
|
}
|
|
1663
908
|
|
|
1664
|
-
/**
|
|
1665
|
-
* Sub-table headers like `[model_providers.opencodex.env_http_headers]` appear when a Codex app
|
|
1666
|
-
* config rewrite re-serializes the provider's inline `env_http_headers` table. They define the
|
|
1667
|
-
* same `model_providers.opencodex` provider, so cleanup must remove them too — otherwise the
|
|
1668
|
-
* provider survives with no `name` and Codex rejects the whole config
|
|
1669
|
-
* ("provider name must not be empty"). The dot terminator keeps a user's
|
|
1670
|
-
* `[model_providers.opencodex_backup]`-style tables out of scope.
|
|
1671
|
-
*/
|
|
1672
|
-
function isOcxProviderHeaderLine(trimmedLine: string): boolean {
|
|
1673
|
-
// Root form matched by regex, not equality: TOML v1.0 allows a trailing comment
|
|
1674
|
-
// (`[model_providers.opencodex] # comment`), and an exact compare would miss that form.
|
|
1675
|
-
// The sub-table prefix check already tolerates trailing comments by construction.
|
|
1676
|
-
return (
|
|
1677
|
-
/^\[model_providers\.opencodex\]\s*(?:#.*)?$/.test(trimmedLine) ||
|
|
1678
|
-
trimmedLine.startsWith("[model_providers.opencodex.")
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function hasOcxProviderTable(content: string): boolean {
|
|
1683
|
-
return content
|
|
1684
|
-
.split("\n")
|
|
1685
|
-
.some((line) => isOcxProviderHeaderLine(line.trim()));
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
function removeOcxSection(content: string): string {
|
|
1689
|
-
const lines = content.split("\n");
|
|
1690
|
-
const filtered: string[] = [];
|
|
1691
|
-
let inOcxSection = false;
|
|
1692
|
-
for (const line of lines) {
|
|
1693
|
-
if (
|
|
1694
|
-
line.includes(OCX_SECTION_MARKER) ||
|
|
1695
|
-
isOcxProviderHeaderLine(line.trim())
|
|
1696
|
-
) {
|
|
1697
|
-
inOcxSection = true;
|
|
1698
|
-
continue;
|
|
1699
|
-
}
|
|
1700
|
-
if (inOcxSection) {
|
|
1701
|
-
// End the injected section at the next table header that ISN'T our own. Exact match on the
|
|
1702
|
-
// provider name (plus our own sub-tables) so a user's
|
|
1703
|
-
// "[model_providers.opencodex_backup]" (or similar) is preserved, not swallowed.
|
|
1704
|
-
if (/^\s*\[/.test(line) && !isOcxProviderHeaderLine(line.trim())) {
|
|
1705
|
-
inOcxSection = false;
|
|
1706
|
-
filtered.push(line);
|
|
1707
|
-
}
|
|
1708
|
-
continue;
|
|
1709
|
-
}
|
|
1710
|
-
filtered.push(line);
|
|
1711
|
-
}
|
|
1712
|
-
return (
|
|
1713
|
-
filtered
|
|
1714
|
-
.join("\n")
|
|
1715
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
1716
|
-
.trimEnd() + "\n"
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
interface StripOpencodexConfigResult {
|
|
1721
|
-
content: string;
|
|
1722
|
-
managedDefaultsError: string | null;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
/**
|
|
1726
|
-
* Detailed form used by the on-disk restore path. A damaged ownership marker is
|
|
1727
|
-
* ambiguous: keep the associated value, but return the transform error so the
|
|
1728
|
-
* caller cannot report a complete restore.
|
|
1729
|
-
*/
|
|
1730
|
-
function stripOpencodexConfigResult(
|
|
1731
|
-
content: string,
|
|
1732
|
-
journaledBaseUrl: string | null = null,
|
|
1733
|
-
journaledRealtimeWsBaseUrl: string | null = null,
|
|
1734
|
-
): StripOpencodexConfigResult {
|
|
1735
|
-
let out = content;
|
|
1736
|
-
const hadRootOcxProvider =
|
|
1737
|
-
readRootTomlString(out, "model_provider") === "opencodex";
|
|
1738
|
-
// #1798: marker adjacency is FORMATTING evidence, and a Codex app rewrite keeps values
|
|
1739
|
-
// while dropping comments. Fall back to VALUE evidence -- the exact URL we recorded
|
|
1740
|
-
// writing -- so an app-rewritten config is still recognized as ours.
|
|
1741
|
-
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out)
|
|
1742
|
-
|| (journaledBaseUrl !== null && rootTomlString(out, "openai_base_url") === journaledBaseUrl);
|
|
1743
|
-
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
1744
|
-
out = stripJournaledOpenaiBaseUrl(out, journaledBaseUrl, journaledRealtimeWsBaseUrl);
|
|
1745
|
-
if (hasOcxProviderTable(out)) {
|
|
1746
|
-
out = removeOcxSection(out);
|
|
1747
|
-
}
|
|
1748
|
-
out = removeProfileSection(out);
|
|
1749
|
-
// Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
|
|
1750
|
-
// must match the detection regex above, or a detected line could survive un-removed.
|
|
1751
|
-
out = out
|
|
1752
|
-
.split("\n")
|
|
1753
|
-
.filter((l) => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l))
|
|
1754
|
-
.join("\n");
|
|
1755
|
-
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
1756
|
-
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
1757
|
-
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
1758
|
-
const managedDefaults = transformManagedSubagentDefaults(out, null);
|
|
1759
|
-
if (managedDefaults.ok) out = managedDefaults.content;
|
|
1760
|
-
out = stripOpencodexCatalogPath(out);
|
|
1761
|
-
return {
|
|
1762
|
-
content: out.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n",
|
|
1763
|
-
managedDefaultsError: !managedDefaults.ok ? managedDefaults.error : null,
|
|
1764
|
-
};
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
/** Pure transform: strip the opencodex provider block + `model_provider = "opencodex"` lines. */
|
|
1768
|
-
export function stripOpencodexConfig(content: string): string {
|
|
1769
|
-
return stripOpencodexConfigResult(content).content;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
function hasOpencodexRouting(content: string): boolean {
|
|
1773
|
-
return (
|
|
1774
|
-
hasOcxProviderTable(content) ||
|
|
1775
|
-
/^\s*model_provider\s*=\s*"opencodex"/m.test(content) ||
|
|
1776
|
-
hasInjectedOpenaiBaseUrl(content)
|
|
1777
|
-
);
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
export function removeCodexConfig(
|
|
1781
|
-
options: { preserveProfile?: boolean } = {},
|
|
1782
|
-
): { success: boolean; message: string } {
|
|
1783
|
-
const historyError = preflightCodexHistoryInjection(false, false);
|
|
1784
|
-
if (historyError) return { success: false, message: `Codex configuration preserved: ${historyError}. Native writer coordination is required.` };
|
|
1785
|
-
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
1786
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1787
|
-
unlinkSync(CODEX_PROFILE_PATH);
|
|
1788
|
-
return {
|
|
1789
|
-
success: true,
|
|
1790
|
-
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
1793
|
-
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
1794
|
-
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
1795
|
-
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
1796
|
-
const eol = dominantEol(rawContent);
|
|
1797
|
-
const content = applyEol(rawContent, "\n");
|
|
1798
|
-
// Read the recorded injection once: the strip below consumes it, and so does the
|
|
1799
|
-
// ownership verdict, which must agree with what was actually removed.
|
|
1800
|
-
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl();
|
|
1801
|
-
const journaledRealtimeWsBaseUrl = journaledInjectedRealtimeWsBaseUrl();
|
|
1802
|
-
const had = hasOpencodexRouting(content)
|
|
1803
|
-
|| (journaledBaseUrl !== null && rootTomlString(content, "openai_base_url") === journaledBaseUrl)
|
|
1804
|
-
|| (journaledRealtimeWsBaseUrl !== null
|
|
1805
|
-
&& rootTomlString(content, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
|
|
1806
|
-
const stripped = stripOpencodexConfigResult(content, journaledBaseUrl, journaledRealtimeWsBaseUrl);
|
|
1807
|
-
if (had || stripped.content !== content) {
|
|
1808
|
-
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
1809
|
-
}
|
|
1810
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1811
|
-
unlinkSync(CODEX_PROFILE_PATH);
|
|
1812
|
-
const removedMessage = had
|
|
1813
|
-
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
1814
|
-
: "opencodex not present in Codex config.";
|
|
1815
|
-
if (stripped.managedDefaultsError) {
|
|
1816
|
-
const routingMessage = had
|
|
1817
|
-
? removedMessage
|
|
1818
|
-
: "No opencodex routing was present in Codex config.";
|
|
1819
|
-
return {
|
|
1820
|
-
success: false,
|
|
1821
|
-
message:
|
|
1822
|
-
`${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
|
|
1823
|
-
"The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
return {
|
|
1827
|
-
success: true,
|
|
1828
|
-
message: removedMessage,
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
|
|
1833
|
-
|
|
1834
|
-
export interface CodexRestoreConfigResult {
|
|
1835
|
-
state: CodexRestoreArtifactState;
|
|
1836
|
-
changed: boolean;
|
|
1837
|
-
action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
|
|
1838
|
-
message: string;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
export interface CodexRestoreCatalogResult {
|
|
1842
|
-
state: CodexRestoreArtifactState;
|
|
1843
|
-
changed: boolean;
|
|
1844
|
-
removed: number;
|
|
1845
|
-
kept: number;
|
|
1846
|
-
path: string | null;
|
|
1847
|
-
message: string;
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
export interface CodexRestoreHistoryResult {
|
|
1851
|
-
state: CodexRestoreArtifactState;
|
|
1852
|
-
changed: boolean;
|
|
1853
|
-
reason?: CodexHistoryFailureReason;
|
|
1854
|
-
rows: number;
|
|
1855
|
-
files: number;
|
|
1856
|
-
ejectedRows: number;
|
|
1857
|
-
message: string;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
export interface CodexNativeRestoreResult {
|
|
1861
|
-
success: boolean;
|
|
1862
|
-
message: string;
|
|
1863
|
-
externalProvider?: string;
|
|
1864
|
-
artifacts: {
|
|
1865
|
-
config: CodexRestoreConfigResult;
|
|
1866
|
-
catalog: CodexRestoreCatalogResult;
|
|
1867
|
-
history: CodexRestoreHistoryResult;
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
function failedHistoryRestore(
|
|
1872
|
-
reason?: CodexHistoryFailureReason,
|
|
1873
|
-
detail?: string,
|
|
1874
|
-
progress: { rows?: number; files?: number } = {},
|
|
1875
|
-
): CodexRestoreHistoryResult {
|
|
1876
|
-
const rows = progress.rows ?? 0;
|
|
1877
|
-
const files = progress.files ?? 0;
|
|
1878
|
-
const changed = rows > 0 || files > 0;
|
|
1879
|
-
return {
|
|
1880
|
-
state: "failed",
|
|
1881
|
-
changed,
|
|
1882
|
-
...(reason ? { reason } : {}),
|
|
1883
|
-
rows,
|
|
1884
|
-
files,
|
|
1885
|
-
ejectedRows: 0,
|
|
1886
|
-
message: reason === "permission"
|
|
1887
|
-
? changed
|
|
1888
|
-
? "Codex resume history changed but did NOT converge because permission was denied while finalizing the backup manifest; the manifest was retained for review and safe retry."
|
|
1889
|
-
: "Codex resume history could NOT be restored because permission was denied."
|
|
1890
|
-
: reason === "busy"
|
|
1891
|
-
? changed
|
|
1892
|
-
? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
|
|
1893
|
-
: detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
|
|
1894
|
-
: reason === "integrity"
|
|
1895
|
-
? changed
|
|
1896
|
-
? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
|
|
1897
|
-
: "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
|
|
1898
|
-
: detail
|
|
1899
|
-
? `Codex resume history could NOT be restored: ${detail}`
|
|
1900
|
-
: "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
|
|
1901
|
-
};
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
/**
|
|
1905
|
-
* Restore failure wording for a Worker outcome.
|
|
1906
|
-
*
|
|
1907
|
-
* Only a genuine busy result blames the Codex app. An unsafe-path refusal, an
|
|
1908
|
-
* unavailable coordinator database, a permission denial, or a dead/timed-out
|
|
1909
|
-
* worker is a different problem; the old collapse made every one of those read
|
|
1910
|
-
* as "the Codex app is holding the database" (issue #1191). `busy` and
|
|
1911
|
-
* `permission` keep the restore-specific sentence built by
|
|
1912
|
-
* `failedHistoryRestore`; every other reason reuses the single formatter so
|
|
1913
|
-
* the two modules cannot drift apart.
|
|
1914
|
-
*/
|
|
1915
|
-
export function failedHistoryRestoreFromOutcome(
|
|
1916
|
-
outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
|
|
1917
|
-
): CodexRestoreHistoryResult {
|
|
1918
|
-
if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
|
|
1919
|
-
if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
|
|
1920
|
-
return failedHistoryRestore(
|
|
1921
|
-
"busy",
|
|
1922
|
-
describeHistoryJobFailure(outcome, "restore"),
|
|
1923
|
-
{ rows: outcome.rows, files: outcome.files },
|
|
1924
|
-
);
|
|
1925
|
-
}
|
|
1926
|
-
if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
|
|
1927
|
-
return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1928
|
-
}
|
|
1929
|
-
if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
|
|
1930
|
-
return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
|
|
1931
|
-
}
|
|
1932
|
-
return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
|
|
1936
|
-
const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
|
|
1937
|
-
return {
|
|
1938
|
-
success: true,
|
|
1939
|
-
message,
|
|
1940
|
-
externalProvider: activeProvider,
|
|
1941
|
-
artifacts: {
|
|
1942
|
-
config: { state: "skipped", changed: false, action: "external-provider-preserved", message },
|
|
1943
|
-
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1944
|
-
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1945
|
-
},
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
/** A foreign service claim is an authority boundary, including explicit CLI restore. */
|
|
1950
|
-
function foreignOwnershipRestoreRefusal(message: string): CodexNativeRestoreResult {
|
|
1951
|
-
return {
|
|
1952
|
-
success: false,
|
|
1953
|
-
message: `Codex native restore refused: ${message}`,
|
|
1954
|
-
artifacts: {
|
|
1955
|
-
config: { state: "skipped", changed: false, action: "failed", message },
|
|
1956
|
-
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1957
|
-
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1958
|
-
},
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
|
|
1963
|
-
const message = "Codex integration was re-enabled; native restore was skipped.";
|
|
1964
|
-
return skippedRestoreEnvelope(true, message);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
/**
|
|
1968
|
-
* A schema-complete all-skipped envelope for outcomes decided before any
|
|
1969
|
-
* restore machinery runs. Every `restore --json` path must stay shape-stable
|
|
1970
|
-
* with `CodexNativeRestoreResult`; consumers never special-case early exits.
|
|
1971
|
-
*/
|
|
1972
|
-
export function skippedRestoreEnvelope(success: boolean, message: string): CodexNativeRestoreResult {
|
|
1973
|
-
return {
|
|
1974
|
-
success,
|
|
1975
|
-
message,
|
|
1976
|
-
artifacts: {
|
|
1977
|
-
config: { state: "skipped", changed: false, action: "owned-fields-stripped", message },
|
|
1978
|
-
catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
|
|
1979
|
-
history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
|
|
1980
|
-
},
|
|
1981
|
-
};
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
/** Config was attempted and failed; downstream artifacts were never attempted. */
|
|
1985
|
-
function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNativeRestoreResult {
|
|
1986
|
-
const result = skippedRestoreEnvelope(false, config.message);
|
|
1987
|
-
result.artifacts.config = config;
|
|
1988
|
-
return result;
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
/** The config/profile half of a native restore, reported as one artifact. */
|
|
1992
|
-
function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
|
|
1993
|
-
const preImages = captureCodexPreImages();
|
|
1994
|
-
const result = restoreCodexConfigInlineImpl(kind);
|
|
1995
|
-
if (result.state === "failed") {
|
|
1996
|
-
const compensated = restoreCodexPreImages(preImages);
|
|
1997
|
-
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
1998
|
-
}
|
|
1999
|
-
return result;
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
|
|
2003
|
-
try {
|
|
2004
|
-
beforeRestoreConfigForTests?.(kind);
|
|
2005
|
-
const historyError = preflightCodexHistoryInjection(false, false);
|
|
2006
|
-
if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };
|
|
2007
|
-
const journal = restoreJournalState();
|
|
2008
|
-
if (journal.unverified) {
|
|
2009
|
-
return {
|
|
2010
|
-
state: "failed", changed: false, action: "failed",
|
|
2011
|
-
message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
const restored = journal.configRestored
|
|
2015
|
-
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
2016
|
-
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
2017
|
-
if (restored.success) {
|
|
2018
|
-
// A successful journal/fallback write can race native history migration too.
|
|
2019
|
-
// Refuse here while preimage compensation and the remove transaction can roll back.
|
|
2020
|
-
const finalHistoryError = preflightCodexHistoryInjection(false, false);
|
|
2021
|
-
if (finalHistoryError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${finalHistoryError}.` };
|
|
2022
|
-
}
|
|
2023
|
-
return restored.success
|
|
2024
|
-
? {
|
|
2025
|
-
state: "ok",
|
|
2026
|
-
changed: journal.configRestored || journal.profileRestored || journal.profileChanged || restored.message.startsWith("Removed"),
|
|
2027
|
-
action: journal.configRestored ? "journal-restored" : "owned-fields-stripped",
|
|
2028
|
-
message: restored.message,
|
|
2029
|
-
}
|
|
2030
|
-
: { state: "failed", changed: false, action: "failed", message: restored.message };
|
|
2031
|
-
} catch (error) {
|
|
2032
|
-
return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
/** The catalog half, always inside its own K acquisition. */
|
|
2037
|
-
/**
|
|
2038
|
-
* The catalog half, always inside its own K acquisition.
|
|
2039
|
-
*
|
|
2040
|
-
* `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
|
|
2041
|
-
* successful journal restore deletes the journal, and a config restore can remove
|
|
2042
|
-
* `model_catalog_json`. Reading it here would be too late in both cases (#1798).
|
|
2043
|
-
*/
|
|
2044
|
-
function restoreCodexCatalogArtifact(
|
|
2045
|
-
revalidateDesiredState: boolean,
|
|
2046
|
-
journaledCatalogPath: string | null,
|
|
2047
|
-
): CodexRestoreCatalogResult {
|
|
2048
|
-
const owningCodexHome = getCodexHome();
|
|
2049
|
-
try {
|
|
2050
|
-
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
2051
|
-
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
2052
|
-
? null
|
|
2053
|
-
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
|
|
2054
|
-
return restored.kind === "completed" && restored.value !== null
|
|
2055
|
-
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
2056
|
-
: restored.kind === "completed"
|
|
2057
|
-
? {
|
|
2058
|
-
state: "skipped", changed: false, removed: 0, kept: 0, path: null,
|
|
2059
|
-
message: "Codex integration was re-enabled; native catalog restoration was skipped.",
|
|
2060
|
-
}
|
|
2061
|
-
: {
|
|
2062
|
-
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
2063
|
-
message: `Codex catalog could not be restored: ${restored.reason}.`,
|
|
2064
|
-
};
|
|
2065
|
-
} catch (error) {
|
|
2066
|
-
return {
|
|
2067
|
-
state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
|
|
2068
|
-
message: error instanceof Error ? error.message : String(error),
|
|
2069
|
-
};
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
/**
|
|
2074
|
-
* Restore native Codex, running history in a Worker under H.
|
|
2075
|
-
*
|
|
2076
|
-
* On a coordinated home the config/profile restore happens INSIDE the Codex
|
|
2077
|
-
* write lock, publishing a `remove` transition — the same serialization inject
|
|
2078
|
-
* uses. Without it, an older restore could overwrite a config a concurrent
|
|
2079
|
-
* enable had just written under the lock, and then honestly report success
|
|
2080
|
-
* while desired intent said ON. The desired-state re-read under the lock turns
|
|
2081
|
-
* that lost race into the discriminated `desired_enabled` skip.
|
|
2082
|
-
*/
|
|
2083
|
-
export async function restoreNativeCodexAsync(
|
|
2084
|
-
options: { revalidateDesiredState?: boolean } = {},
|
|
2085
|
-
): Promise<CodexNativeRestoreResult> {
|
|
2086
|
-
try {
|
|
2087
|
-
return await restoreNativeCodexAsyncImpl(options);
|
|
2088
|
-
} catch (error) {
|
|
2089
|
-
if (!(error instanceof CodexRestoreRefusal)) throw error;
|
|
2090
|
-
return failedConfigRestoreEnvelope(error.config);
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
async function restoreNativeCodexAsyncImpl(
|
|
2095
|
-
options: { revalidateDesiredState?: boolean },
|
|
2096
|
-
): Promise<CodexNativeRestoreResult> {
|
|
2097
|
-
const activeProvider = currentExternalCodexModelProvider();
|
|
2098
|
-
if (activeProvider) {
|
|
2099
|
-
// External-provider courtesy: only the stale journal is removed. The
|
|
2100
|
-
// history worker must not launch — it would turn a read-mostly courtesy
|
|
2101
|
-
// result into a history mutation on a home we do not own.
|
|
2102
|
-
removeJournal();
|
|
2103
|
-
return externalProviderRestoreResult(activeProvider);
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
// `restore` normally honours a human request even when an unrelated
|
|
2107
|
-
// service-manager probe is unavailable. A recorded FOREIGN home is not an
|
|
2108
|
-
// unrelated probe: it is positive evidence another installation owns these
|
|
2109
|
-
// native artifacts, so do not create profile/claim locks before refusing.
|
|
2110
|
-
if (options.revalidateDesiredState) {
|
|
2111
|
-
const ownership = inspectNativeCodexOwnership();
|
|
2112
|
-
if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
|
|
2113
|
-
if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
const historyError = preflightCodexHistoryInjection(false, false);
|
|
2117
|
-
if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
|
|
2118
|
-
|
|
2119
|
-
const eligibility = codexWriteCoordinationEligibility({
|
|
2120
|
-
coordinatorPath: () =>
|
|
2121
|
-
resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
|
|
2122
|
-
residue: () => classifyNativeRoutedResidue(),
|
|
2123
|
-
integrationRecord: () => readIntegrationRecord(),
|
|
2124
|
-
});
|
|
2125
|
-
|
|
2126
|
-
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
2127
|
-
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
2128
|
-
// catalog we actually wrote (#1798).
|
|
2129
|
-
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
2130
|
-
let config: CodexRestoreConfigResult;
|
|
2131
|
-
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
2132
|
-
|
|
2133
|
-
if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
|
|
2134
|
-
// The restore has no candidate bytes to witness; freshness comes from the
|
|
2135
|
-
// filesystem reads and the desired-state re-read performed under the lock.
|
|
2136
|
-
const witness = { authoritySnapshotId: "codex-native-restore" };
|
|
2137
|
-
const coordinated = await withCodexWriteLock(
|
|
2138
|
-
{
|
|
2139
|
-
timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
2140
|
-
...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
|
|
2141
|
-
admitted: witness,
|
|
2142
|
-
readAdmissionUnderLock: () => witness,
|
|
2143
|
-
},
|
|
2144
|
-
(ctx) => {
|
|
2145
|
-
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
2146
|
-
throw new CodexWriteLockSkipped("desired_enabled");
|
|
2147
|
-
}
|
|
2148
|
-
const published = ctx.coordinator.beginTransition(
|
|
2149
|
-
{
|
|
2150
|
-
nativeGeneration: ctx.expectation.nativeBefore,
|
|
2151
|
-
currentTxId: ctx.currentTxId,
|
|
2152
|
-
},
|
|
2153
|
-
{
|
|
2154
|
-
txId: ctx.expectation.txId,
|
|
2155
|
-
direction: "remove",
|
|
2156
|
-
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
2157
|
-
nextRetryAt: new Date().toISOString(),
|
|
2158
|
-
},
|
|
2159
|
-
);
|
|
2160
|
-
if (published.kind !== "updated") {
|
|
2161
|
-
throw new CodexWriteConflictError(
|
|
2162
|
-
`The Codex transition could not be published: ${published.kind}.`,
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
const preImages = captureCodexPreImages();
|
|
2166
|
-
let restored: CodexRestoreConfigResult;
|
|
2167
|
-
try {
|
|
2168
|
-
restored = restoreCodexConfigInline(eligibility.kind);
|
|
2169
|
-
// Throw inside N so the published remove transition rolls back too.
|
|
2170
|
-
if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
|
|
2171
|
-
} catch (error) {
|
|
2172
|
-
const compensated = restoreCodexPreImages(preImages);
|
|
2173
|
-
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
2174
|
-
throw error;
|
|
2175
|
-
}
|
|
2176
|
-
return {
|
|
2177
|
-
config: restored,
|
|
2178
|
-
preImages,
|
|
2179
|
-
receipt: {
|
|
2180
|
-
nativeGeneration: ctx.expectation.nativeAfter,
|
|
2181
|
-
currentTxId: ctx.expectation.txId,
|
|
2182
|
-
},
|
|
2183
|
-
};
|
|
2184
|
-
},
|
|
2185
|
-
);
|
|
2186
|
-
if (coordinated.status === "skipped") return desiredEnabledRestoreSkip();
|
|
2187
|
-
if (coordinated.status !== "acquired") {
|
|
2188
|
-
config = {
|
|
2189
|
-
state: "failed",
|
|
2190
|
-
changed: false,
|
|
2191
|
-
action: "failed",
|
|
2192
|
-
message: coordinated.status === "busy"
|
|
2193
|
-
? `Another process is writing Codex configuration right now (waited ${coordinated.waitedMs}ms). Retry shortly.`
|
|
2194
|
-
: `Codex configuration was not restored: ${coordinated.message}`,
|
|
2195
|
-
};
|
|
2196
|
-
} else {
|
|
2197
|
-
recordCodexNativeTransactionProvenance(
|
|
2198
|
-
coordinated.value.preImages,
|
|
2199
|
-
coordinated.value.receipt.currentTxId,
|
|
2200
|
-
);
|
|
2201
|
-
config = coordinated.value.config;
|
|
2202
|
-
transitionReceipt = coordinated.value.receipt;
|
|
2203
|
-
}
|
|
2204
|
-
} else {
|
|
2205
|
-
// Legacy-uncoordinated (or unresolvable) homes keep the unserialized path
|
|
2206
|
-
// they have always had; restore is the escape hatch and must not strand
|
|
2207
|
-
// them. The plain re-read still honors an intervening re-enable.
|
|
2208
|
-
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
2209
|
-
return desiredEnabledRestoreSkip();
|
|
2210
|
-
}
|
|
2211
|
-
config = restoreCodexConfigInline(eligibility.kind);
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
2215
|
-
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
2216
|
-
const outcome = await runCodexHistoryJob({
|
|
2217
|
-
...resolveCodexHistoryJobTarget(),
|
|
2218
|
-
...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
|
|
2219
|
-
operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
|
|
2220
|
-
});
|
|
2221
|
-
if (transitionReceipt) {
|
|
2222
|
-
resolveCodexHistoryTransition(transitionReceipt, outcome);
|
|
2223
|
-
}
|
|
2224
|
-
const history: CodexRestoreHistoryResult = outcome.kind === "converged"
|
|
2225
|
-
? {
|
|
2226
|
-
state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
|
|
2227
|
-
message: outcome.rows > 0
|
|
2228
|
-
? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
|
|
2229
|
-
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
2230
|
-
}
|
|
2231
|
-
: outcome.kind === "skipped"
|
|
2232
|
-
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
|
|
2233
|
-
: outcome.kind === "blocked" && (outcome.reason === "desired_disabled" || outcome.reason === "desired_enabled")
|
|
2234
|
-
? {
|
|
2235
|
-
state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
|
|
2236
|
-
message: outcome.reason === "desired_disabled"
|
|
2237
|
-
? "Codex integration was disabled; history restoration was skipped."
|
|
2238
|
-
: "Codex integration was enabled; history restoration was skipped.",
|
|
2239
|
-
}
|
|
2240
|
-
: outcome.kind === "blocked" || outcome.kind === "failed"
|
|
2241
|
-
? failedHistoryRestoreFromOutcome(outcome)
|
|
2242
|
-
: failedHistoryRestore();
|
|
2243
|
-
const base = catalog.removed > 0
|
|
2244
|
-
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
2245
|
-
: config.message;
|
|
2246
|
-
const success = catalog.state !== "failed"
|
|
2247
|
-
&& history.state !== "failed";
|
|
2248
|
-
return {
|
|
2249
|
-
success,
|
|
2250
|
-
message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
|
|
2251
|
-
artifacts: { config, catalog, history },
|
|
2252
|
-
};
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
|
|
2256
|
-
const activeProvider = currentExternalCodexModelProvider();
|
|
2257
|
-
if (activeProvider) {
|
|
2258
|
-
removeJournal();
|
|
2259
|
-
return externalProviderRestoreResult(activeProvider);
|
|
2260
|
-
}
|
|
2261
|
-
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
2262
|
-
return desiredEnabledRestoreSkip();
|
|
2263
|
-
}
|
|
2264
|
-
const historyError = preflightCodexHistoryInjection(false, false);
|
|
2265
|
-
if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
|
|
2266
|
-
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
2267
|
-
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
2268
|
-
// catalog we actually wrote (#1798).
|
|
2269
|
-
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
2270
|
-
const config = restoreCodexConfigInline();
|
|
2271
|
-
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
2272
|
-
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
2273
|
-
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
2274
|
-
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
2275
|
-
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
2276
|
-
// Legacy (non-loopback) installs keep the unconditional write-open restore.
|
|
2277
|
-
let skipWhenProvablyNoop = false;
|
|
2278
|
-
try {
|
|
2279
|
-
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
2280
|
-
} catch {
|
|
2281
|
-
/* unreadable config: keep the conservative write-open restore */
|
|
2282
|
-
}
|
|
2283
|
-
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
2284
|
-
// native files come down here, and history runs in the Worker under H.
|
|
2285
|
-
const rawHistory = options.skipHistory
|
|
2286
|
-
? { rows: 0, files: 0 }
|
|
2287
|
-
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
2288
|
-
skipWhenProvablyNoop,
|
|
2289
|
-
});
|
|
2290
|
-
const history: CodexRestoreHistoryResult = options.skipHistory
|
|
2291
|
-
? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
|
|
2292
|
-
: rawHistory.failed
|
|
2293
|
-
? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
|
|
2294
|
-
: {
|
|
2295
|
-
state: "ok",
|
|
2296
|
-
changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
|
|
2297
|
-
rows: rawHistory.rows,
|
|
2298
|
-
files: rawHistory.files,
|
|
2299
|
-
ejectedRows: rawHistory.ejectedRows ?? 0,
|
|
2300
|
-
message: rawHistory.rows > 0
|
|
2301
|
-
? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
|
|
2302
|
-
: "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
|
|
2303
|
-
};
|
|
2304
|
-
const message = catalog.removed > 0
|
|
2305
|
-
? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
|
|
2306
|
-
: config.message;
|
|
2307
|
-
return {
|
|
2308
|
-
success: catalog.state !== "failed" && history.state !== "failed",
|
|
2309
|
-
message,
|
|
2310
|
-
artifacts: { config, catalog, history },
|
|
2311
|
-
};
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
909
|
export function getCodexConfigPath(): string {
|
|
2315
910
|
return CODEX_CONFIG_PATH;
|
|
2316
911
|
}
|
|
@@ -2340,3 +935,53 @@ export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legac
|
|
|
2340
935
|
: "Codex resume history NOT changed";
|
|
2341
936
|
return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
|
|
2342
937
|
}
|
|
938
|
+
|
|
939
|
+
export {
|
|
940
|
+
providerBaseHost,
|
|
941
|
+
standaloneCodexRoutingTarget,
|
|
942
|
+
} from "./inject/routing-target";
|
|
943
|
+
export type { CodexRoutingTarget } from "./inject/routing-target";
|
|
944
|
+
|
|
945
|
+
export {
|
|
946
|
+
applyEol,
|
|
947
|
+
buildOpenaiBaseUrlLine,
|
|
948
|
+
buildProfileFile,
|
|
949
|
+
buildProviderTableBlock,
|
|
950
|
+
buildRealtimeWsBaseUrlLine,
|
|
951
|
+
chooseCatalogPathForInjection,
|
|
952
|
+
currentExternalCodexModelProvider,
|
|
953
|
+
dominantEol,
|
|
954
|
+
externalCodexModelProvider,
|
|
955
|
+
setRootOpenaiBaseUrl,
|
|
956
|
+
setRootRealtimeWsBaseUrl,
|
|
957
|
+
stripInjectedOpenaiBaseUrl,
|
|
958
|
+
stripRootContextWindowOverrides,
|
|
959
|
+
} from "./inject/config-toml";
|
|
960
|
+
|
|
961
|
+
export {
|
|
962
|
+
classifyCodexRouting,
|
|
963
|
+
getCodexRoutingKind,
|
|
964
|
+
isCodexRoutingInjected,
|
|
965
|
+
} from "./inject/routing-classify";
|
|
966
|
+
export type { CodexRoutingKind } from "./inject/routing-classify";
|
|
967
|
+
|
|
968
|
+
export {
|
|
969
|
+
removeCodexConfig,
|
|
970
|
+
stripOpencodexConfig,
|
|
971
|
+
} from "./inject/remove";
|
|
972
|
+
|
|
973
|
+
export type {
|
|
974
|
+
CodexNativeRestoreResult,
|
|
975
|
+
CodexRestoreArtifactState,
|
|
976
|
+
CodexRestoreCatalogResult,
|
|
977
|
+
CodexRestoreConfigResult,
|
|
978
|
+
CodexRestoreHistoryResult,
|
|
979
|
+
} from "./inject/restore";
|
|
980
|
+
export {
|
|
981
|
+
failedHistoryRestoreFromOutcome,
|
|
982
|
+
restoreNativeCodex,
|
|
983
|
+
restoreNativeCodexAsync,
|
|
984
|
+
setBeforeRestoreConfigForTests,
|
|
985
|
+
skippedRestoreEnvelope,
|
|
986
|
+
} from "./inject/restore";
|
|
987
|
+
|