@bitkyc08/opencodex 2.58.0 → 2.59.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/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { parseStrictSemver } from "../lib/strict-semver";
|
|
4
|
+
|
|
5
|
+
/** Explicit observation, not runtime selection or authority to run an installer. */
|
|
6
|
+
export interface CodexCliInstallationIdentityInput {
|
|
7
|
+
readonly candidate: string;
|
|
8
|
+
readonly npmPrefix: string;
|
|
9
|
+
readonly npmCli: string;
|
|
10
|
+
readonly node: string;
|
|
11
|
+
/** Where the candidate came from; defaults to an explicit CLI argument. */
|
|
12
|
+
readonly candidateSource?: "explicit-cli" | "selected";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CodexCliInstallationIdentityReport {
|
|
16
|
+
readonly schemaVersion: 1;
|
|
17
|
+
readonly candidateSource: "explicit-cli" | "selected";
|
|
18
|
+
readonly status: "observed" | "refused";
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
readonly installationIdentityObserved: boolean;
|
|
21
|
+
readonly selectionAttested: false;
|
|
22
|
+
readonly managed: false;
|
|
23
|
+
readonly applyAllowed: false;
|
|
24
|
+
readonly packageVersion: string | null;
|
|
25
|
+
readonly npmVersion: string | null;
|
|
26
|
+
readonly identityDigest: string | null;
|
|
27
|
+
readonly proof: "windows-handle-bound" | null;
|
|
28
|
+
readonly toolchain: "observed-only";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InstallationFileRequest {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly maxBytes: number;
|
|
34
|
+
readonly hashOnly?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ObservedInstallationFile {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly identity: {
|
|
40
|
+
readonly volumeSerial: string;
|
|
41
|
+
readonly fileId: string;
|
|
42
|
+
readonly size: number;
|
|
43
|
+
readonly lastWriteTime: string;
|
|
44
|
+
readonly changeTime: string;
|
|
45
|
+
};
|
|
46
|
+
readonly bytes: Uint8Array;
|
|
47
|
+
readonly digest?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type InstallationFilesResult =
|
|
51
|
+
| { readonly kind: "observed"; readonly files: readonly ObservedInstallationFile[] }
|
|
52
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
53
|
+
|
|
54
|
+
export interface CodexCliInstallationIdentityDeps {
|
|
55
|
+
readonly platform?: NodeJS.Platform;
|
|
56
|
+
readonly inspectFiles?: (requests: readonly InstallationFileRequest[]) => Promise<InstallationFilesResult>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const TEXT_LIMIT = 256 * 1024;
|
|
60
|
+
const NODE_LIMIT = 256 * 1024 * 1024;
|
|
61
|
+
const hash = (value: string | Uint8Array): string => createHash("sha256").update(value).digest("hex");
|
|
62
|
+
// NTFS can enable case-sensitive directory entries. Do not collapse different
|
|
63
|
+
// paths merely because ordinary Windows directories usually ignore case.
|
|
64
|
+
const key = (path: string): string => path[0]!.toUpperCase() + path.slice(1);
|
|
65
|
+
|
|
66
|
+
function localPath(value: string): string | null {
|
|
67
|
+
if (typeof value !== "string" || value.length > 30_000 || !/^[a-z]:[\\/]/i.test(value)) return null;
|
|
68
|
+
const path = value.replaceAll("/", "\\");
|
|
69
|
+
const parts = path.slice(3).split("\\");
|
|
70
|
+
if (!parts.length || parts.some(part => !part || part === "." || part === ".."
|
|
71
|
+
|| /[\x00-\x1f<>:"|?*]/.test(part) || /[. ]$/.test(part)
|
|
72
|
+
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) return null;
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function supportedLayoutPath(path: string): boolean {
|
|
77
|
+
// This slice does not adopt package managers or packaged desktop applications.
|
|
78
|
+
return !/(?:^|\\)(?:windowsapps|scoop|\.volta|\.nvm|\.asdf|\.mise|\.fnm|\.nvs|\.nodenv)(?:\\|$)|\.app(?:\\|$)/i.test(path);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function report(
|
|
82
|
+
reason: string,
|
|
83
|
+
versions?: { codex: string; npm: string; digest: string },
|
|
84
|
+
candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
|
|
85
|
+
): CodexCliInstallationIdentityReport {
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
schemaVersion: 1, candidateSource, status: versions ? "observed" : "refused",
|
|
88
|
+
reason, installationIdentityObserved: !!versions, selectionAttested: false, managed: false,
|
|
89
|
+
applyAllowed: false, packageVersion: versions?.codex ?? null, npmVersion: versions?.npm ?? null,
|
|
90
|
+
identityDigest: versions?.digest ?? null, proof: versions ? "windows-handle-bound" : null,
|
|
91
|
+
toolchain: "observed-only",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A refusal before any observation ran, e.g. when no selected candidate can be identified. */
|
|
96
|
+
export function codexCliInstallationRefusal(
|
|
97
|
+
reason: string,
|
|
98
|
+
candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
|
|
99
|
+
): CodexCliInstallationIdentityReport {
|
|
100
|
+
return report(reason, undefined, candidateSource);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function manifest(file: ObservedInstallationFile, name: string, bin: string, expected: string): string | null {
|
|
104
|
+
try {
|
|
105
|
+
const value: unknown = JSON.parse(Buffer.from(file.bytes).toString("utf8"));
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
107
|
+
const item = value as Record<string, unknown>;
|
|
108
|
+
const entry = typeof item.bin === "string" ? item.bin
|
|
109
|
+
: item.bin && typeof item.bin === "object" ? (item.bin as Record<string, unknown>)[bin] : null;
|
|
110
|
+
return item.name === name && typeof item.version === "string" && parseStrictSemver(item.version)
|
|
111
|
+
&& entry === expected ? item.version : null;
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Only the standard npm cmd-shim is linked here; arbitrary wrappers remain unverified. */
|
|
116
|
+
function npmCodexCommandShim(bytes: Uint8Array): boolean {
|
|
117
|
+
const expected = `@ECHO off
|
|
118
|
+
GOTO start
|
|
119
|
+
:find_dp0
|
|
120
|
+
SET dp0=%~dp0
|
|
121
|
+
EXIT /b
|
|
122
|
+
:start
|
|
123
|
+
SETLOCAL
|
|
124
|
+
CALL :find_dp0
|
|
125
|
+
|
|
126
|
+
IF EXIST "%dp0%\\node.exe" (
|
|
127
|
+
SET "_prog=%dp0%\\node.exe"
|
|
128
|
+
) ELSE (
|
|
129
|
+
SET "_prog=node"
|
|
130
|
+
SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\@openai\\codex\\bin\\codex.js" %*`;
|
|
134
|
+
const normalize = (text: string) => text.replaceAll("\r\n", "\n").trimEnd();
|
|
135
|
+
return normalize(Buffer.from(bytes).toString("utf8")) === normalize(expected);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fileDigest(file: ObservedInstallationFile): string {
|
|
139
|
+
return file.digest ?? hash(file.bytes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fileEvidence(file: ObservedInstallationFile): readonly (string | number)[] {
|
|
143
|
+
const id = file.identity;
|
|
144
|
+
return [key(file.path), id.volumeSerial, id.fileId, id.size, id.lastWriteTime, id.changeTime, fileDigest(file)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** No filesystem or native-library work occurs until this explicit operation is called. */
|
|
148
|
+
export async function inspectCodexCliInstallationIdentity(
|
|
149
|
+
input: CodexCliInstallationIdentityInput,
|
|
150
|
+
deps: CodexCliInstallationIdentityDeps = {},
|
|
151
|
+
): Promise<CodexCliInstallationIdentityReport> {
|
|
152
|
+
const source = input.candidateSource ?? "explicit-cli";
|
|
153
|
+
const refused = (reason: string) => report(reason, undefined, source);
|
|
154
|
+
if ((deps.platform ?? process.platform) !== "win32") return refused("unsupported_platform");
|
|
155
|
+
const candidate = localPath(input.candidate);
|
|
156
|
+
const prefix = localPath(input.npmPrefix);
|
|
157
|
+
const npmCli = localPath(input.npmCli);
|
|
158
|
+
const node = localPath(input.node);
|
|
159
|
+
if (!candidate || !prefix || !npmCli || !node) return refused("unsafe_path");
|
|
160
|
+
if (![candidate, prefix, npmCli, node].every(supportedLayoutPath)) return refused("unsupported_layout");
|
|
161
|
+
const packageRoot = win32.join(prefix, "node_modules", "@openai", "codex");
|
|
162
|
+
const codexManifest = win32.join(packageRoot, "package.json");
|
|
163
|
+
const codexBin = win32.join(packageRoot, "bin", "codex.js");
|
|
164
|
+
const shim = win32.join(prefix, "codex.cmd");
|
|
165
|
+
// A codex.opencodex-real.cmd backup is npm's renamed launcher: same grammar, same dir.
|
|
166
|
+
const backingShim = win32.join(prefix, "codex.opencodex-real.cmd");
|
|
167
|
+
const acceptedCandidates = source === "selected"
|
|
168
|
+
? [key(codexBin), key(shim), key(backingShim)]
|
|
169
|
+
: [key(codexBin), key(shim)];
|
|
170
|
+
if (!acceptedCandidates.includes(key(candidate))
|
|
171
|
+
|| !/\\node_modules\\npm\\bin\\npm-cli\.js$/i.test(npmCli)
|
|
172
|
+
|| win32.basename(node).toLowerCase() !== "node.exe") return refused("unsupported_layout");
|
|
173
|
+
const npmManifest = win32.join(win32.dirname(win32.dirname(npmCli)), "package.json");
|
|
174
|
+
try {
|
|
175
|
+
const inspect = deps.inspectFiles ?? (await import("./windows-installation-files")).inspectWindowsInstallationFiles;
|
|
176
|
+
// Read both manifests again with every linked file held open: first-pass bytes cannot
|
|
177
|
+
// authorize links after a manifest replacement between the two observations.
|
|
178
|
+
const first = await inspect([{ path: codexManifest, maxBytes: TEXT_LIMIT }, { path: npmManifest, maxBytes: TEXT_LIMIT }]);
|
|
179
|
+
if (first.kind !== "observed") return refused("native_proof_unavailable");
|
|
180
|
+
const initial = new Map(first.files.map(file => [key(file.path), file]));
|
|
181
|
+
const codex = initial.get(key(codexManifest));
|
|
182
|
+
const npm = initial.get(key(npmManifest));
|
|
183
|
+
if (!codex || !npm) return refused("read_failed");
|
|
184
|
+
const codexVersion = manifest(codex, "@openai/codex", "codex", "bin/codex.js");
|
|
185
|
+
const npmVersion = manifest(npm, "npm", "npm", "bin/npm-cli.js");
|
|
186
|
+
if (!codexVersion || !npmVersion) return refused("package_mismatch");
|
|
187
|
+
const requests: InstallationFileRequest[] = [codexManifest, npmManifest, candidate, codexBin, npmCli]
|
|
188
|
+
.filter((path, index, all) => all.findIndex(other => key(other) === key(path)) === index)
|
|
189
|
+
.map(path => ({ path, maxBytes: TEXT_LIMIT }));
|
|
190
|
+
requests.push({ path: node, maxBytes: NODE_LIMIT, hashOnly: true });
|
|
191
|
+
const second = await inspect(requests);
|
|
192
|
+
if (second.kind !== "observed") return refused("native_proof_unavailable");
|
|
193
|
+
const files = new Map(second.files.map(file => [key(file.path), file]));
|
|
194
|
+
if (requests.some(request => !files.has(key(request.path)))) return refused("read_failed");
|
|
195
|
+
for (const before of [codex, npm]) {
|
|
196
|
+
if (JSON.stringify(fileEvidence(before)) !== JSON.stringify(fileEvidence(files.get(key(before.path))!))) {
|
|
197
|
+
return refused("identity_changed");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if ([key(shim), key(backingShim)].includes(key(candidate))
|
|
201
|
+
&& !npmCodexCommandShim(files.get(key(candidate))!.bytes)) {
|
|
202
|
+
return refused("launcher_mismatch");
|
|
203
|
+
}
|
|
204
|
+
const observed = [...files.values()].sort((a, b) => key(a.path) < key(b.path) ? -1 : key(a.path) > key(b.path) ? 1 : 0);
|
|
205
|
+
if (observed.some(file => !/^[a-f0-9]{64}$/i.test(fileDigest(file)))) return refused("read_failed");
|
|
206
|
+
const digest = hash(JSON.stringify(["opencodex-explicit-installation-observation-v1", key(prefix),
|
|
207
|
+
key(candidate), key(npmCli), key(node), ...observed.map(fileEvidence)]));
|
|
208
|
+
return report("identity_observed", { codex: codexVersion, npm: npmVersion, digest }, source);
|
|
209
|
+
} catch { return refused("read_failed"); }
|
|
210
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { SHIM_MARKER } from "./shim-templates";
|
|
4
|
+
import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The proof-captured launcher snapshot used to identify the selected candidate.
|
|
8
|
+
* Every field is optional because older launchers and direct Bun/source runs
|
|
9
|
+
* supply no snapshot at all; derivation fails closed rather than reading the
|
|
10
|
+
* ambient environment.
|
|
11
|
+
*/
|
|
12
|
+
export interface CodexCliInstallationSnapshot {
|
|
13
|
+
readonly codexCliPath?: string | null;
|
|
14
|
+
readonly path?: string | null;
|
|
15
|
+
readonly pathExt?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CodexCliInstallationTargetRefusal =
|
|
19
|
+
| "unsupported_platform"
|
|
20
|
+
| "candidate_unavailable"
|
|
21
|
+
| "unsupported_layout"
|
|
22
|
+
| "toolchain_unresolved";
|
|
23
|
+
|
|
24
|
+
export type CodexCliInstallationTargetDerivation =
|
|
25
|
+
| { readonly kind: "derived"; readonly input: CodexCliInstallationIdentityInput }
|
|
26
|
+
| { readonly kind: "unavailable"; readonly reason: CodexCliInstallationTargetRefusal };
|
|
27
|
+
|
|
28
|
+
export interface CodexCliInstallationTargetDeps {
|
|
29
|
+
readonly platform?: NodeJS.Platform;
|
|
30
|
+
readonly exists?: (path: string) => boolean;
|
|
31
|
+
/** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
|
|
32
|
+
readonly fileContains?: (path: string, marker: string) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
|
|
36
|
+
const SHIM_PROBE_BYTES = 8 * 1024;
|
|
37
|
+
const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
|
|
38
|
+
|
|
39
|
+
function defaultFileContains(path: string, marker: string): boolean {
|
|
40
|
+
let descriptor: number | undefined;
|
|
41
|
+
let contains = false;
|
|
42
|
+
try {
|
|
43
|
+
descriptor = openSync(path, "r");
|
|
44
|
+
const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
|
|
45
|
+
const count = readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
46
|
+
contains = bytes.subarray(0, count).toString("utf8").includes(marker);
|
|
47
|
+
} catch {
|
|
48
|
+
contains = false;
|
|
49
|
+
} finally {
|
|
50
|
+
if (descriptor !== undefined) {
|
|
51
|
+
try { closeSync(descriptor); } catch { contains = false; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return contains;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* First match wins, mirroring PATH resolution: directories in order, and within
|
|
59
|
+
* each directory every PATHEXT suffix in order (or the exact name when it
|
|
60
|
+
* already carries an extension). Skipping a hit to keep scanning would attest
|
|
61
|
+
* something other than the launcher that actually resolves.
|
|
62
|
+
*/
|
|
63
|
+
function scanPath(
|
|
64
|
+
name: string,
|
|
65
|
+
pathValue: string | null | undefined,
|
|
66
|
+
pathExt: string | null | undefined,
|
|
67
|
+
exists: (path: string) => boolean,
|
|
68
|
+
): string | null {
|
|
69
|
+
const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
|
|
70
|
+
const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
|
|
71
|
+
for (const entry of (pathValue ?? "").split(";")) {
|
|
72
|
+
const dir = entry.trim().replace(/^"+|"+$/g, "");
|
|
73
|
+
if (!dir) continue;
|
|
74
|
+
for (const candidateName of names) {
|
|
75
|
+
const candidate = win32.join(dir, candidateName);
|
|
76
|
+
if (exists(candidate)) return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Derive the four attestation inputs from the proof-captured launcher snapshot.
|
|
84
|
+
* Discovery only proposes candidate paths; the held-handle observation remains
|
|
85
|
+
* the authority, so a stale or racing probe can only produce a refusal, never a
|
|
86
|
+
* false identity. No ambient environment is read: without a snapshot the result
|
|
87
|
+
* is unavailable rather than silently trusting the child's environment.
|
|
88
|
+
*/
|
|
89
|
+
export function deriveCodexCliInstallationInput(
|
|
90
|
+
snapshot: CodexCliInstallationSnapshot,
|
|
91
|
+
deps: CodexCliInstallationTargetDeps = {},
|
|
92
|
+
): CodexCliInstallationTargetDerivation {
|
|
93
|
+
if ((deps.platform ?? process.platform) !== "win32") {
|
|
94
|
+
return { kind: "unavailable", reason: "unsupported_platform" };
|
|
95
|
+
}
|
|
96
|
+
const exists = deps.exists ?? existsSync;
|
|
97
|
+
const fileContains = deps.fileContains ?? defaultFileContains;
|
|
98
|
+
const configured = snapshot.codexCliPath;
|
|
99
|
+
|
|
100
|
+
let candidate: string | null;
|
|
101
|
+
if (configured) {
|
|
102
|
+
if (/^[a-z]:[\\/]/i.test(configured)) {
|
|
103
|
+
if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
104
|
+
candidate = win32.normalize(configured);
|
|
105
|
+
} else {
|
|
106
|
+
if (configured.includes("/") || configured.includes("\\")) {
|
|
107
|
+
return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
108
|
+
}
|
|
109
|
+
candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
|
|
110
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
|
|
114
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
|
|
118
|
+
// artifact. The renamed original beside it is the file npm wrote.
|
|
119
|
+
if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
|
|
120
|
+
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
|
|
121
|
+
if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
|
|
122
|
+
candidate = backing;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const base = win32.basename(candidate).toLowerCase();
|
|
126
|
+
let prefix: string;
|
|
127
|
+
if (base === "codex.cmd" || base === "codex.opencodex-real.cmd") {
|
|
128
|
+
prefix = win32.dirname(candidate);
|
|
129
|
+
} else if (candidate.toLowerCase().endsWith(CODEX_PACKAGE_SUFFIX)) {
|
|
130
|
+
prefix = candidate.slice(0, candidate.length - CODEX_PACKAGE_SUFFIX.length);
|
|
131
|
+
} else {
|
|
132
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
133
|
+
}
|
|
134
|
+
if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
|
|
135
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
|
|
139
|
+
let node = win32.join(prefix, "node.exe");
|
|
140
|
+
if (!exists(node)) {
|
|
141
|
+
const resolved = scanPath("node.exe", snapshot.path, null, exists);
|
|
142
|
+
if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
143
|
+
node = resolved;
|
|
144
|
+
}
|
|
145
|
+
const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
|
|
146
|
+
if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
kind: "derived",
|
|
150
|
+
input: Object.freeze({
|
|
151
|
+
candidate,
|
|
152
|
+
npmPrefix: prefix,
|
|
153
|
+
npmCli,
|
|
154
|
+
node,
|
|
155
|
+
candidateSource: "selected",
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
package/src/codex/convergence.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
catalogHasRoutedEntries,
|
|
37
37
|
findSupportedNativeTemplate,
|
|
38
38
|
legacyCatalogBackupPath,
|
|
39
|
+
nativeMultiAgentDefaults,
|
|
39
40
|
parseCatalogJson,
|
|
40
41
|
type RawCatalog,
|
|
41
42
|
type RawEntry,
|
|
@@ -69,6 +70,7 @@ import {
|
|
|
69
70
|
clampCatalogModelsToObservedCodexSupport,
|
|
70
71
|
supportedCodexReasoningEffortsFromObservedCatalog,
|
|
71
72
|
} from "./catalog/effort";
|
|
73
|
+
import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
|
|
72
74
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
73
75
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
|
|
74
76
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
@@ -305,6 +307,7 @@ function prepareCatalog(
|
|
|
305
307
|
[catalog.models ?? [], ...nativeRecoverySources],
|
|
306
308
|
);
|
|
307
309
|
const catalogModels = nativeCatalogModels;
|
|
310
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(config, ordered, catalogModels);
|
|
308
311
|
const routedEntries = buildCatalogEntriesFromObservedState({
|
|
309
312
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
310
313
|
gptSlugs: [],
|
|
@@ -371,8 +374,10 @@ function prepareCatalog(
|
|
|
371
374
|
includeNativeOpenAi,
|
|
372
375
|
accountBoundEntries,
|
|
373
376
|
suppressedBareNativeSlugs,
|
|
377
|
+
suppressedSyntheticMaxSlugs,
|
|
374
378
|
openaiContextCap,
|
|
375
379
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
380
|
+
nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels),
|
|
376
381
|
policy: {
|
|
377
382
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
378
383
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
4
4
|
import { zstdDecompressSync } from "node:zlib";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { resolveCodexStateDbPath } from "./paths";
|
|
7
|
+
import { openCodexStateForPreflight } from "./history-state-open";
|
|
7
8
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
8
9
|
import {
|
|
9
10
|
CODEX_HISTORY_RESUMABLE_SOURCES,
|
|
@@ -433,7 +434,9 @@ export function preflightCodexHistoryInjection(
|
|
|
433
434
|
if (!existsSync(resolvedPath)) {
|
|
434
435
|
return restoreEntries.length > 0 ? "history_state_database_missing" : null;
|
|
435
436
|
}
|
|
436
|
-
|
|
437
|
+
// Read-only, and narrowed so a cleanly-closed WAL store is inspected rather than refused
|
|
438
|
+
// (#4943). The open order is the safety property; see history-state-open.ts.
|
|
439
|
+
db = openCodexStateForPreflight(resolvedPath);
|
|
437
440
|
const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
|
|
438
441
|
const paginatedColumn = columns.some(column => column.name === "history_mode");
|
|
439
442
|
if (paginatedColumn && restoreEntries.length > 0) return HISTORY_RELABEL_STANDS_DOWN;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the read-only Codex history preflight opens the state store.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `history-provider.ts` deliberately. That file is the single largest module in
|
|
5
|
+
* `src/codex/` and sits just under the repository's 2000-line ratchet; the reasoning below is
|
|
6
|
+
* load-bearing and long, and appending it there would have pushed the file over. Keeping the
|
|
7
|
+
* open policy in one small module also puts the decision somewhere it can be read on its own,
|
|
8
|
+
* which matters because getting it wrong is silent in both directions.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { Database, constants as sqliteConstants } from "bun:sqlite";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read-only open flags that skip WAL shared memory entirely.
|
|
16
|
+
*
|
|
17
|
+
* `immutable=1` has to arrive as a `file:` URI, and a URI filename needs SQLITE_OPEN_URI.
|
|
18
|
+
* Same idiom as the storage scanner, the log-guard inspector and the coordinator doctor, for
|
|
19
|
+
* the same reason: never touch a foreign store's WAL protocol.
|
|
20
|
+
*/
|
|
21
|
+
const IMMUTABLE_READONLY_FLAGS = sqliteConstants.SQLITE_OPEN_READONLY | sqliteConstants.SQLITE_OPEN_URI;
|
|
22
|
+
|
|
23
|
+
/** Is this the "there is no shared memory and I may not create it" open failure? */
|
|
24
|
+
export function isStateDbCantOpenError(error: unknown): boolean {
|
|
25
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
26
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
27
|
+
// Matched on the code with a message fallback, exactly like classifyRecoverableHistoryError:
|
|
28
|
+
// the same SQLite condition reaches us as a code on some platforms and as bare text on others.
|
|
29
|
+
return code === "SQLITE_CANTOPEN" || message.includes("unable to open database file");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Which step of the primary attempt a test wants to fail. */
|
|
33
|
+
export type StateDbPreflightOpenPhase = "open" | "first-read";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Test-only knob: force the primary attempt to fail with a supplied error.
|
|
37
|
+
*
|
|
38
|
+
* The fallback below turns on a condition this repository cannot reproduce deterministically
|
|
39
|
+
* from a test: whether a plain read-only open of a cleanly-closed WAL store fails or quietly
|
|
40
|
+
* creates the `-shm` depends on the platform VFS and on the directory the store sits in.
|
|
41
|
+
* Pinning the NARROWING — sidecars absent admits the immutable read, either sidecar present
|
|
42
|
+
* still refuses — therefore needs the failure supplied rather than provoked, or the test would
|
|
43
|
+
* assert the host's SQLite build instead of this decision (#4943).
|
|
44
|
+
*
|
|
45
|
+
* The phase exists because the platforms disagree about WHEN the condition is raised, not only
|
|
46
|
+
* about whether it is: see the first-read note on `openCodexStateForPreflight`.
|
|
47
|
+
*/
|
|
48
|
+
let openFailureForTests: ((path: string, phase: StateDbPreflightOpenPhase) => unknown) | undefined;
|
|
49
|
+
export function setStateDbPreflightOpenFailureForTests(hook: typeof openFailureForTests): void {
|
|
50
|
+
openFailureForTests = hook;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Open the Codex state store for the read-only preflight.
|
|
55
|
+
*
|
|
56
|
+
* `{ readonly: true }` is tried FIRST and stays the primary path, because it is the only mode
|
|
57
|
+
* that can see a live WAL: it joins the writer's shared memory, so a thread another process
|
|
58
|
+
* just migrated to paginated history is visible here and the preflight refuses on it. An
|
|
59
|
+
* immutable open reads the last checkpointed main database instead. Making that the primary
|
|
60
|
+
* path would trade a refusal for a stale snapshot, and a refusal this preflight fails to
|
|
61
|
+
* observe is a config transition that proceeds over history Codex owns — the exact outcome the
|
|
62
|
+
* whole guard exists to prevent.
|
|
63
|
+
*
|
|
64
|
+
* A WAL store closed cleanly is the case that has no shared memory to join. SQLite cannot
|
|
65
|
+
* create the `-shm` from a read-only connection, so the open fails SQLITE_CANTOPEN on a store
|
|
66
|
+
* that is perfectly healthy, the catch-all in the preflight folds that into
|
|
67
|
+
* `history_injection_preflight_unavailable`, and `ocx sync` refuses on every attempt with no
|
|
68
|
+
* way forward (#4943).
|
|
69
|
+
*
|
|
70
|
+
* So the fallback is admitted only in the state where the absent sidecars are what make an
|
|
71
|
+
* immutable read exact rather than stale: no `-wal` and no `-shm` on disk means no writer is
|
|
72
|
+
* attached and no committed content sits outside the main database, so the main file IS the
|
|
73
|
+
* whole store and the two modes cannot disagree. Either sidecar present keeps the original
|
|
74
|
+
* error and the refusal that follows from it — a `-wal` holds content this connection would
|
|
75
|
+
* not read, and a `-shm` means a writer is attached, and neither is a store this preflight
|
|
76
|
+
* may inspect from a snapshot.
|
|
77
|
+
*
|
|
78
|
+
* The first read belongs INSIDE this attempt. `sqlite3_open_v2` does not touch page 1, so a
|
|
79
|
+
* store whose header says WAL is not inspected until the first prepare — which is where the
|
|
80
|
+
* missing shared memory is discovered on macOS, one caller frame above this function. Opening
|
|
81
|
+
* here and reading there put the classification and the failure in different scopes: the
|
|
82
|
+
* fallback was never reached, and the operator got the catch-all refusal the fix was supposed
|
|
83
|
+
* to remove. Linux hides this because its SQLite materializes the sidecars on that first read
|
|
84
|
+
* and never fails at all (#4943, macOS CI).
|
|
85
|
+
*/
|
|
86
|
+
export function openCodexStateForPreflight(resolvedPath: string): Database {
|
|
87
|
+
let db: Database | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const forced = openFailureForTests?.(resolvedPath, "open");
|
|
90
|
+
if (forced) throw forced;
|
|
91
|
+
db = new Database(resolvedPath, { readonly: true });
|
|
92
|
+
const forcedRead = openFailureForTests?.(resolvedPath, "first-read");
|
|
93
|
+
if (forcedRead) throw forcedRead;
|
|
94
|
+
// Page 1, read while the failure is still this function's to classify.
|
|
95
|
+
db.query<{ tables: number }, []>("SELECT count(*) AS tables FROM sqlite_master").get();
|
|
96
|
+
return db;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
db?.close();
|
|
99
|
+
if (!isStateDbCantOpenError(error)) throw error;
|
|
100
|
+
if (existsSync(`${resolvedPath}-wal`) || existsSync(`${resolvedPath}-shm`)) throw error;
|
|
101
|
+
// pathToFileURL percent-encodes the reserved characters a naive `file:${path}` would
|
|
102
|
+
// misparse as a query or fragment.
|
|
103
|
+
return new Database(`${pathToFileURL(resolvedPath).href}?immutable=1`, IMMUTABLE_READONLY_FLAGS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -55,6 +55,46 @@ export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
|
55
55
|
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Label Codex shows for the injected provider when the operator has not chosen one. */
|
|
59
|
+
export const DEFAULT_CODEX_PROVIDER_DISPLAY_NAME = "OpenCodex Proxy";
|
|
60
|
+
|
|
61
|
+
/** Longest label accepted, matching the display-label policy used for provider names. */
|
|
62
|
+
const MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH = 128;
|
|
63
|
+
|
|
64
|
+
/** Would this label put a control character into config.toml? */
|
|
65
|
+
function hasControlCharacter(value: string): boolean {
|
|
66
|
+
// Checked by code point rather than by a control-character regex, which needs a lint
|
|
67
|
+
// suppression this repository's hygiene gate rejects — and which reads no more clearly.
|
|
68
|
+
for (const character of value) {
|
|
69
|
+
const code = character.codePointAt(0) ?? 0;
|
|
70
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which label to write, given whatever the config holds.
|
|
77
|
+
*
|
|
78
|
+
* Presentation only, and deliberately separate from identity: routing resolves through the
|
|
79
|
+
* provider id `opencodex` in the root `model_provider` line and the `[model_providers.opencodex]`
|
|
80
|
+
* header, neither of which is derived from this value. So a rename cannot reroute a thread or
|
|
81
|
+
* orphan a row that already names that id (#4810).
|
|
82
|
+
*
|
|
83
|
+
* Every rejected value falls back to the default rather than being emitted or omitted. Codex
|
|
84
|
+
* refuses to load a provider with no name, so writing a blank one would break the whole config
|
|
85
|
+
* file rather than one thread — strictly worse than the branding it was meant to remove. That is
|
|
86
|
+
* also why there is no way to suppress the field: suppression here means choosing a neutral
|
|
87
|
+
* label. A control character or an over-long value is rejected for the same reason, because
|
|
88
|
+
* `tomlString` would faithfully encode something Codex may still reject.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveCodexProviderDisplayName(configured?: string): string {
|
|
91
|
+
const trimmed = (configured ?? "").trim();
|
|
92
|
+
if (!trimmed) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
93
|
+
if (trimmed.length > MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
94
|
+
if (hasControlCharacter(trimmed)) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
|
|
58
98
|
export function buildProviderTableBlock(
|
|
59
99
|
port: number,
|
|
60
100
|
supportsWebsockets?: boolean,
|
|
@@ -84,12 +124,13 @@ export function buildProviderTableBlock(
|
|
|
84
124
|
export function buildProviderTableBlockForTarget(
|
|
85
125
|
target: CodexRoutingTarget,
|
|
86
126
|
supportsWebsockets = false,
|
|
127
|
+
displayName?: string,
|
|
87
128
|
): string {
|
|
88
129
|
const lines = [
|
|
89
130
|
"",
|
|
90
131
|
OCX_SECTION_MARKER,
|
|
91
132
|
"[model_providers.opencodex]",
|
|
92
|
-
|
|
133
|
+
`name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
|
|
93
134
|
`base_url = ${tomlString(target.baseUrl)}`,
|
|
94
135
|
'wire_api = "responses"',
|
|
95
136
|
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
@@ -518,6 +559,7 @@ export function buildProfileFileForTarget(
|
|
|
518
559
|
catalogPath?: string | null,
|
|
519
560
|
supportsWebsockets = false,
|
|
520
561
|
fastMode?: boolean,
|
|
562
|
+
displayName?: string,
|
|
521
563
|
): string {
|
|
522
564
|
const origin = routingTargetOrigin(target);
|
|
523
565
|
const host = new URL(origin).host;
|
|
@@ -542,7 +584,7 @@ export function buildProfileFileForTarget(
|
|
|
542
584
|
];
|
|
543
585
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
544
586
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
545
|
-
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
587
|
+
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets, displayName).trimEnd(), "");
|
|
546
588
|
return lines.join("\n");
|
|
547
589
|
}
|
|
548
590
|
|
package/src/codex/inject.ts
CHANGED
|
@@ -369,7 +369,7 @@ async function injectCodexConfigImpl(
|
|
|
369
369
|
content =
|
|
370
370
|
content.trimEnd() +
|
|
371
371
|
"\n" +
|
|
372
|
-
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
|
|
372
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
|
|
373
373
|
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
374
374
|
// are the Design B ones: a user's own root line is never replaced.
|
|
375
375
|
if (keepRootOverrideAlongsideTable) {
|
|
@@ -430,6 +430,7 @@ async function injectCodexConfigImpl(
|
|
|
430
430
|
catalogPath,
|
|
431
431
|
websocketsEnabled(config ?? {}),
|
|
432
432
|
config?.fastMode,
|
|
433
|
+
config?.codexProviderDisplayName,
|
|
433
434
|
);
|
|
434
435
|
content = applyEol(content, eol);
|
|
435
436
|
|
|
@@ -492,7 +493,7 @@ async function injectCodexConfigImpl(
|
|
|
492
493
|
*/
|
|
493
494
|
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
494
495
|
content = applyEol(
|
|
495
|
-
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})),
|
|
496
|
+
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
|
|
496
497
|
eol,
|
|
497
498
|
);
|
|
498
499
|
}
|