@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CODE_MODE_EXEC_TOOL_NAME,
|
|
3
|
+
dottedToolName,
|
|
3
4
|
namespacedToolName,
|
|
4
5
|
normalizeDeclaredToolName,
|
|
5
6
|
} from "../types";
|
|
@@ -74,16 +75,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
74
75
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* A dotted spelling is a safe alias only when it cannot ALSO be read as some other identity's
|
|
80
|
+
* canonical `ns__name`.
|
|
81
|
+
*
|
|
82
|
+
* `{namespace: "x__y", name: "z"}` produces the dotted spelling "x__y.z", which is exactly the
|
|
83
|
+
* canonical wire name of `{namespace: "x", name: "y.z"}`. If only the latter is declared, an
|
|
84
|
+
* echoed call for the former would still find "x__y.z" in the declared set and be authorized as
|
|
85
|
+
* a tool the caller never granted. Requiring both halves to be free of the `__` separator keeps
|
|
86
|
+
* a dotted alias from ever impersonating a canonical name.
|
|
87
|
+
*/
|
|
88
|
+
function dottedAliasIsUnambiguous(namespace: string, name: string): boolean {
|
|
89
|
+
return !namespace.includes("__") && !name.includes("__");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wireToolInnerName(tool: unknown): string | undefined {
|
|
93
|
+
if (!isPlainObject(tool)) return undefined;
|
|
79
94
|
const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
|
|
80
95
|
? tool.function
|
|
81
96
|
: undefined;
|
|
82
|
-
|
|
97
|
+
return typeof tool.name === "string" && tool.name.length > 0
|
|
83
98
|
? tool.name
|
|
84
99
|
: typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
|
|
85
100
|
? nestedFunction.name
|
|
86
101
|
: undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addWireToolName(
|
|
105
|
+
names: Set<string>,
|
|
106
|
+
tool: unknown,
|
|
107
|
+
namespace?: string,
|
|
108
|
+
ambiguousDottedAliases?: ReadonlySet<string>,
|
|
109
|
+
): void {
|
|
110
|
+
if (!isPlainObject(tool)) return;
|
|
111
|
+
const name = wireToolInnerName(tool);
|
|
87
112
|
if (!name) return;
|
|
88
113
|
// Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
|
|
89
114
|
// as a bare inner name or as the flattened form; accept both rather than guess which
|
|
@@ -93,6 +118,17 @@ function addWireToolName(names: Set<string>, tool: unknown, namespace?: string):
|
|
|
93
118
|
return;
|
|
94
119
|
}
|
|
95
120
|
names.add(namespacedToolName(namespace, name));
|
|
121
|
+
// Some routed providers echo the flattened wire name with a dot (`ns.name`, observed with
|
|
122
|
+
// muse-spark via opencode-go) instead of `ns__name`. It is the same tool identity, so register
|
|
123
|
+
// the dotted spelling too, mirroring `toolChoiceAliases` (#3402) -- but only while that
|
|
124
|
+
// spelling names exactly one declared tool. Dots are legal inside both a namespace and a
|
|
125
|
+
// name, so two distinct identities can flatten onto one dotted alias; accepting it then would
|
|
126
|
+
// authorize a call the caller never declared under that identity. Ambiguous aliases fall back
|
|
127
|
+
// to the unambiguous `ns__name` form.
|
|
128
|
+
const dotted = dottedToolName(namespace, name);
|
|
129
|
+
if (dottedAliasIsUnambiguous(namespace, name) && !ambiguousDottedAliases?.has(dotted)) {
|
|
130
|
+
names.add(dotted);
|
|
131
|
+
}
|
|
96
132
|
// `exec` is the one name that also switches on nested-helper normalization, so a bare alias
|
|
97
133
|
// for a namespaced MCP tool would silently authorize `exec_command`/`shell_command`/
|
|
98
134
|
// `apply_patch` the request never declared. Every other inner name keeps the bare alias.
|
|
@@ -118,19 +154,65 @@ export function currentTurnWireToolCatalogBody(
|
|
|
118
154
|
return { ...body, input: body.input.slice(start) };
|
|
119
155
|
}
|
|
120
156
|
|
|
121
|
-
function addWireToolSpecs(
|
|
157
|
+
function addWireToolSpecs(
|
|
158
|
+
names: Set<string>,
|
|
159
|
+
specs: unknown,
|
|
160
|
+
ambiguousDottedAliases?: ReadonlySet<string>,
|
|
161
|
+
): void {
|
|
122
162
|
if (!Array.isArray(specs)) return;
|
|
123
163
|
for (const spec of specs) {
|
|
124
164
|
if (!isPlainObject(spec)) continue;
|
|
125
165
|
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
126
166
|
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
127
|
-
for (const inner of spec.tools) addWireToolName(names, inner, namespace);
|
|
167
|
+
for (const inner of spec.tools) addWireToolName(names, inner, namespace, ambiguousDottedAliases);
|
|
128
168
|
continue;
|
|
129
169
|
}
|
|
130
|
-
addWireToolName(names, spec);
|
|
170
|
+
addWireToolName(names, spec, undefined, ambiguousDottedAliases);
|
|
131
171
|
}
|
|
132
172
|
}
|
|
133
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Dotted aliases that more than one declared identity would claim, plus dotted aliases that
|
|
176
|
+
* collide with a canonical or bare declared name.
|
|
177
|
+
*
|
|
178
|
+
* Resolved over the WHOLE catalog before any name is registered, so which identity "wins" can
|
|
179
|
+
* never depend on declaration order -- an order the caller controls.
|
|
180
|
+
*/
|
|
181
|
+
function collectAmbiguousDottedAliases(specGroups: readonly unknown[]): Set<string> {
|
|
182
|
+
const owners = new Map<string, string | null>();
|
|
183
|
+
const claim = (alias: string, identity: string): void => {
|
|
184
|
+
const owner = owners.get(alias);
|
|
185
|
+
if (owner === undefined) owners.set(alias, identity);
|
|
186
|
+
else if (owner !== identity) owners.set(alias, null);
|
|
187
|
+
};
|
|
188
|
+
for (const specs of specGroups) {
|
|
189
|
+
if (!Array.isArray(specs)) continue;
|
|
190
|
+
for (const spec of specs) {
|
|
191
|
+
if (!isPlainObject(spec)) continue;
|
|
192
|
+
if (spec.type === "namespace" && Array.isArray(spec.tools)) {
|
|
193
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
194
|
+
if (!namespace || namespace === BUILTIN_FUNCTIONS_NAMESPACE) continue;
|
|
195
|
+
for (const inner of spec.tools) {
|
|
196
|
+
const name = wireToolInnerName(inner);
|
|
197
|
+
if (!name) continue;
|
|
198
|
+
const identity = JSON.stringify([namespace, name]);
|
|
199
|
+
claim(dottedToolName(namespace, name), identity);
|
|
200
|
+
// A canonical or bare name already owned by a different identity poisons the dotted
|
|
201
|
+
// alias that would shadow it.
|
|
202
|
+
claim(namespacedToolName(namespace, name), identity);
|
|
203
|
+
claim(name, identity);
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const name = wireToolInnerName(spec);
|
|
208
|
+
if (name) claim(name, JSON.stringify([undefined, name]));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const ambiguous = new Set<string>();
|
|
212
|
+
for (const [alias, owner] of owners) if (owner === null) ambiguous.add(alias);
|
|
213
|
+
return ambiguous;
|
|
214
|
+
}
|
|
215
|
+
|
|
134
216
|
/**
|
|
135
217
|
* Tool names the OUTBOUND Responses body actually declared.
|
|
136
218
|
*
|
|
@@ -142,15 +224,17 @@ function addWireToolSpecs(names: Set<string>, specs: unknown): void {
|
|
|
142
224
|
export function collectDeclaredWireToolNames(body: unknown): Set<string> {
|
|
143
225
|
const names = new Set<string>();
|
|
144
226
|
if (!isPlainObject(body)) return names;
|
|
145
|
-
|
|
227
|
+
const specGroups: unknown[] = [body.tools];
|
|
146
228
|
if (Array.isArray(body.input)) {
|
|
147
229
|
for (const item of body.input) {
|
|
148
230
|
if (
|
|
149
231
|
isPlainObject(item)
|
|
150
232
|
&& (item.type === "additional_tools" || item.type === "tool_search_output")
|
|
151
|
-
)
|
|
233
|
+
) specGroups.push(item.tools);
|
|
152
234
|
}
|
|
153
235
|
}
|
|
236
|
+
const ambiguousDottedAliases = collectAmbiguousDottedAliases(specGroups);
|
|
237
|
+
for (const specs of specGroups) addWireToolSpecs(names, specs, ambiguousDottedAliases);
|
|
154
238
|
return names;
|
|
155
239
|
}
|
|
156
240
|
|
|
@@ -293,7 +377,15 @@ function undeclaredNameInItem(
|
|
|
293
377
|
if (typeof item.namespace === "string") {
|
|
294
378
|
// Namespaced calls are matched by their full wire name only — never legacy-normalize
|
|
295
379
|
// them, or an undeclared namespaced `exec_command` could slip through as bare `exec`.
|
|
380
|
+
// Both flattened spellings (`ns__name` and the dotted `ns.name` some providers echo,
|
|
381
|
+
// #3402) name the same tool identity.
|
|
296
382
|
if (declared.has(namespacedToolName(item.namespace, name))) return undefined;
|
|
383
|
+
// Only consult the dotted spelling when it cannot double as another identity's canonical
|
|
384
|
+
// name; otherwise a stranger's `ns__name` would authorize this call.
|
|
385
|
+
if (
|
|
386
|
+
dottedAliasIsUnambiguous(item.namespace, name)
|
|
387
|
+
&& declared.has(dottedToolName(item.namespace, name))
|
|
388
|
+
) return undefined;
|
|
297
389
|
return name;
|
|
298
390
|
}
|
|
299
391
|
const effectiveName = normalizeDeclaredToolName(name, declared);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const SAFE_RESPONSE_HEADER_EXACT = new Set([
|
|
2
|
+
"retry-after", "x-request-id", "openai-request-id", "x-codex-turn-state",
|
|
3
|
+
"openai-model", "x-models-etag", "x-reasoning-included",
|
|
4
|
+
"x-codex-credits-has-credits", "x-codex-credits-unlimited", "x-codex-credits-balance",
|
|
5
|
+
"x-codex-promo-message", "x-codex-safety-buffering-enabled", "x-codex-safety-buffering-faster-model",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
/** Response metadata may carry only the same non-credential headers as native WS errors. */
|
|
9
|
+
export function isSafeResponseHeader(name: string): boolean {
|
|
10
|
+
const lower = name.toLowerCase();
|
|
11
|
+
return SAFE_RESPONSE_HEADER_EXACT.has(lower)
|
|
12
|
+
|| lower.startsWith("x-ratelimit-")
|
|
13
|
+
|| /^x-codex(?:-[a-z0-9-]+)?-(primary|secondary|tertiary)-(used-percent|window-minutes|reset-at)$/.test(lower)
|
|
14
|
+
|| /^x-codex(?:-[a-z0-9-]+)?-limit-name$/.test(lower);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function safeResponseHeaders(headers: Headers): Record<string, string> {
|
|
18
|
+
const out: Record<string, string> = {};
|
|
19
|
+
for (const [name, value] of headers) {
|
|
20
|
+
if (isSafeResponseHeader(name)) out[name.toLowerCase()] = value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
package/src/server/search.ts
CHANGED
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
CodexThreadAffinityExpiredError,
|
|
20
20
|
} from "../codex/auth-context";
|
|
21
21
|
import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
|
|
22
|
+
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
23
|
+
import { isCodexReserveRequestEligible } from "../codex/loopback-target";
|
|
24
|
+
import type { DataPlaneAdmission } from "./auth-cors";
|
|
22
25
|
import { formatCodexProviderForLog } from "../codex/routing";
|
|
23
26
|
import { signalWithTimeout } from "../lib/abort";
|
|
24
27
|
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
@@ -52,6 +55,7 @@ export async function handleSearch(
|
|
|
52
55
|
config: OcxConfig,
|
|
53
56
|
logCtx: RequestLogContext,
|
|
54
57
|
turnAdmissionLease?: AdmissionLease,
|
|
58
|
+
admission?: DataPlaneAdmission,
|
|
55
59
|
): Promise<Response> {
|
|
56
60
|
try { validateForwardAdmissionCredential(req.headers, config); }
|
|
57
61
|
catch (err) {
|
|
@@ -93,6 +97,10 @@ export async function handleSearch(
|
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
if (isCodexReserveRequestEligible(config, admission) && (exactAccount?.modelId ?? model) === NATIVE_RESERVE_MODEL) {
|
|
101
|
+
return formatErrorResponse(400, "invalid_request_error",
|
|
102
|
+
"Luna Reserve compatibility is only available as a conversation model, not the standalone search relay. Choose another search model.");
|
|
103
|
+
}
|
|
96
104
|
const candidates = listOpenAiForwardSidecarCandidates(config);
|
|
97
105
|
if (candidates.length === 0) {
|
|
98
106
|
return formatErrorResponse(
|
|
@@ -107,6 +115,7 @@ export async function handleSearch(
|
|
|
107
115
|
try {
|
|
108
116
|
upstream = await resolveFirstUsableOpenAiSidecar(candidates, req.headers, config, {
|
|
109
117
|
exactAccount,
|
|
118
|
+
admission,
|
|
110
119
|
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
111
120
|
});
|
|
112
121
|
if (!upstream) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mutatePersistedConfig } from "../config";
|
|
2
|
+
import { migrateSubagentModels } from "../config/subagent-models";
|
|
3
|
+
import type { OcxConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
/** Rebase the upgrade on disk so another startup cannot shift the roster twice. */
|
|
6
|
+
export function migrateStartupSubagentModels(config: OcxConfig): OcxConfig {
|
|
7
|
+
const projection = { ...config };
|
|
8
|
+
if (!migrateSubagentModels(projection)) return config;
|
|
9
|
+
try {
|
|
10
|
+
const outcome = mutatePersistedConfig(fresh => ({
|
|
11
|
+
changed: migrateSubagentModels(fresh),
|
|
12
|
+
value: fresh,
|
|
13
|
+
}));
|
|
14
|
+
if (outcome.status === "unavailable") {
|
|
15
|
+
console.warn(`[subagent-models-migration] Persistence unavailable (${outcome.reason}); using the upgraded roster in memory only.`);
|
|
16
|
+
} else {
|
|
17
|
+
// Called before live consumers are initialized: later startup saves must
|
|
18
|
+
// use the whole rebased document, not stale unrelated fields from loadConfig.
|
|
19
|
+
return outcome.value;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// A contended coordinator or failed atomic write must not prevent proxy startup.
|
|
23
|
+
// Do not log the exception: filesystem errors may contain private paths.
|
|
24
|
+
console.warn("[subagent-models-migration] Persistence failed; using the upgraded roster in memory only.");
|
|
25
|
+
}
|
|
26
|
+
return projection;
|
|
27
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../config";
|
|
4
|
+
import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
|
|
5
|
+
import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
|
|
6
|
+
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
7
|
+
import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
|
|
8
|
+
import type { OcxConfig } from "../types";
|
|
9
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Does the opencodex dummy marker belong in the system environment?
|
|
13
|
+
*
|
|
14
|
+
* Keyed on the SAME resolver `ocx claude` uses, so an auto config with no Claude auth
|
|
15
|
+
* also reaches plain `claude` launches — before this, auto-absent users got nothing
|
|
16
|
+
* from auto-connect and the feature looked broken for exactly the people it helps
|
|
17
|
+
* (devlog 260726_claude_auth_auto/035).
|
|
18
|
+
*
|
|
19
|
+
* NOTE this is a SNAPSHOT: the file only changes when this runs (proxy start, `ocx
|
|
20
|
+
* ensure`, or a settings save). `ocx claude` re-resolves live on every launch.
|
|
21
|
+
*/
|
|
22
|
+
export type SystemEnvDeps = {
|
|
23
|
+
/** Test seam; production uses the authenticated Node-launcher context. */
|
|
24
|
+
preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
|
|
25
|
+
/** Test seam for auth sources; `env` and `ownTokens` stay bound below. */
|
|
26
|
+
authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bun may synthesize Anthropic variables from a project `.env` before this module runs.
|
|
31
|
+
* Only values recorded by the plain-Node launcher are trusted as parent exports. Direct
|
|
32
|
+
* Bun/service launches have no proof-bound slot list, so they fail closed and let the
|
|
33
|
+
* file/keychain auth sources decide instead of allowing dotenv to select subscription mode.
|
|
34
|
+
*/
|
|
35
|
+
function systemEnvAnthropicEnv(
|
|
36
|
+
env: NodeJS.ProcessEnv,
|
|
37
|
+
preBunAnthropicSlots: readonly AnthropicParentEnvSlot[] | null | undefined,
|
|
38
|
+
): NodeJS.ProcessEnv {
|
|
39
|
+
const trustedSlots = preBunAnthropicSlots === undefined
|
|
40
|
+
? trustedNodeLauncherContext()?.anthropicEnvSlots ?? []
|
|
41
|
+
: preBunAnthropicSlots ?? [];
|
|
42
|
+
const exported = new Set<AnthropicParentEnvSlot>(trustedSlots);
|
|
43
|
+
const sanitized = { ...env };
|
|
44
|
+
for (const name of ANTHROPIC_PARENT_ENV_SLOTS) {
|
|
45
|
+
if (sanitized[name] !== undefined && !exported.has(name)) delete sanitized[name];
|
|
46
|
+
}
|
|
47
|
+
return sanitized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function systemEnvMarkerMode(config: OcxConfig, deps: SystemEnvDeps = {}): "proxy" | "subscription" {
|
|
51
|
+
const env = systemEnvAnthropicEnv(process.env, deps.preBunAnthropicSlots);
|
|
52
|
+
const ownTokens = ownAdmissionTokens(config);
|
|
53
|
+
return resolveClaudeAuthMode(config, detectClaudeAuth({
|
|
54
|
+
...defaultAuthDetectDeps(env, ownTokens),
|
|
55
|
+
...(deps.authDetect ?? {}),
|
|
56
|
+
env: () => env,
|
|
57
|
+
ownTokens,
|
|
58
|
+
})).markerMode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Shell-hook env file: written on inject, sourced by the shell hook in .zshrc.
|
|
63
|
+
// This works for ALL new shells immediately, unlike launchctl setenv which only
|
|
64
|
+
// reaches processes launched directly by launchd (not Terminal.app children).
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export function getShellEnvFilePath(): string {
|
|
68
|
+
return join(getConfigDir(), "claude-env.sh");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function shellValue(value: string): string {
|
|
72
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeShellEnvFile(
|
|
76
|
+
port: number,
|
|
77
|
+
config: OcxConfig,
|
|
78
|
+
modelEnv: Record<string, string> = {},
|
|
79
|
+
auto?: AutoContextMode,
|
|
80
|
+
deps: SystemEnvDeps = {},
|
|
81
|
+
): void {
|
|
82
|
+
const lines = [
|
|
83
|
+
`# Generated by opencodex — do not edit manually`,
|
|
84
|
+
`export ANTHROPIC_BASE_URL=${shellValue(`http://127.0.0.1:${port}`)}`,
|
|
85
|
+
`export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=${shellValue("1")}`,
|
|
86
|
+
];
|
|
87
|
+
// New lever keys are CONDITIONAL exports (audit 139 R2#1): a value the user already
|
|
88
|
+
// exported in their shell wins even though launchctl knows nothing about it.
|
|
89
|
+
const conditional = (name: string, value: string) =>
|
|
90
|
+
`[ -z "\${${name}+x}" ] && export ${name}=${shellValue(value)}`;
|
|
91
|
+
if (systemEnvMarkerMode(config, deps) === "proxy") {
|
|
92
|
+
if (config.apiKeys?.length) {
|
|
93
|
+
lines.push(`export ANTHROPIC_AUTH_TOKEN=${shellValue(config.apiKeys[0].key)}`);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(conditional("ANTHROPIC_AUTH_TOKEN", PROXY_MARKER));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Model slots (default + tiers + legacy small-fast) with [1m] applied (devlog 260712 B2).
|
|
99
|
+
if (modelEnv.ANTHROPIC_MODEL) {
|
|
100
|
+
lines.push(`export ANTHROPIC_MODEL=${shellValue(modelEnv.ANTHROPIC_MODEL)}`);
|
|
101
|
+
} else if (config.claudeCode?.model) {
|
|
102
|
+
lines.push(`export ANTHROPIC_MODEL=${shellValue(config.claudeCode.model)}`);
|
|
103
|
+
}
|
|
104
|
+
for (const [name, value] of Object.entries(modelEnv)) {
|
|
105
|
+
if (name === "ANTHROPIC_MODEL") continue;
|
|
106
|
+
lines.push(conditional(name, value));
|
|
107
|
+
}
|
|
108
|
+
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
109
|
+
if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
|
|
110
|
+
lines.push(conditional("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx))));
|
|
111
|
+
lines.push(conditional("DISABLE_COMPACT", "1"));
|
|
112
|
+
}
|
|
113
|
+
// Auto-context (devlog 260712 020): same contract as `ocx claude` / launchctl.
|
|
114
|
+
const autoShell = auto ?? resolveAutoContext(config.claudeCode);
|
|
115
|
+
if (autoShell.enabled) lines.push(conditional("CLAUDE_CODE_AUTO_COMPACT_WINDOW", String(autoShell.compactWindow)));
|
|
116
|
+
if (config.claudeCode?.alwaysEnableEffort === true) {
|
|
117
|
+
lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
|
|
118
|
+
}
|
|
119
|
+
const shellEnvPath = getShellEnvFilePath();
|
|
120
|
+
recordOwnedConfigPath(getConfigDir(), shellEnvPath);
|
|
121
|
+
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
122
|
+
writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function removeShellEnvFile(): void {
|
|
126
|
+
try { unlinkSync(getShellEnvFilePath()); } catch { /* already gone */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// .zshrc hook auto-install: adds a one-liner that sources claude-env.sh.
|
|
131
|
+
// Idempotent — skips if the hook line already exists.
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
const SHELL_HOOK_MARKER = "# opencodex claude-env hook";
|
|
135
|
+
const SHELL_HOOK_LINE = `${SHELL_HOOK_MARKER}\n[ -f ~/.opencodex/claude-env.sh ] && source ~/.opencodex/claude-env.sh`;
|
|
136
|
+
|
|
137
|
+
export function installShellHook(): { installed: boolean; reason?: string } {
|
|
138
|
+
if (process.platform !== "darwin") return { installed: false, reason: "not macOS" };
|
|
139
|
+
const home = process.env.HOME;
|
|
140
|
+
if (!home) return { installed: false, reason: "no HOME" };
|
|
141
|
+
const zshrcPath = join(home, ".zshrc");
|
|
142
|
+
try {
|
|
143
|
+
let content = "";
|
|
144
|
+
try { content = readFileSync(zshrcPath, "utf8"); } catch { /* file doesn't exist yet */ }
|
|
145
|
+
if (content.includes(SHELL_HOOK_MARKER)) return { installed: false, reason: "already installed" };
|
|
146
|
+
const addition = `\n${SHELL_HOOK_LINE}\n`;
|
|
147
|
+
writeFileSync(zshrcPath, content + addition, { encoding: "utf8", mode: 0o644 });
|
|
148
|
+
return { installed: true };
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return { installed: false, reason: `write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function uninstallShellHook(): { removed: boolean; reason?: string } {
|
|
155
|
+
if (process.platform !== "darwin") return { removed: false, reason: "not macOS" };
|
|
156
|
+
const home = process.env.HOME;
|
|
157
|
+
if (!home) return { removed: false, reason: "no HOME" };
|
|
158
|
+
const zshrcPath = join(home, ".zshrc");
|
|
159
|
+
try {
|
|
160
|
+
const content = readFileSync(zshrcPath, "utf8");
|
|
161
|
+
if (!content.includes(SHELL_HOOK_MARKER)) return { removed: false, reason: "not installed" };
|
|
162
|
+
// Match CR?LF, not LF alone. A .zshrc with CRLF line endings — ordinary on a home
|
|
163
|
+
// directory an editor or another OS has touched — did not match, so the file was
|
|
164
|
+
// rewritten unchanged and the caller was told the hook was removed. Reporting success
|
|
165
|
+
// while the hook still sources on every new shell is the worse of the two failures.
|
|
166
|
+
const cleaned = content.replace(/\r?\n?# opencodex claude-env hook\r?\n\[.*claude-env\.sh.*(?:\r?\n)?/g, "\n");
|
|
167
|
+
// Verify instead of assuming: if the marker survives, the block is shaped in a way this
|
|
168
|
+
// pattern does not own, and the honest answer is failure rather than a silent no-op.
|
|
169
|
+
if (cleaned.includes(SHELL_HOOK_MARKER)) {
|
|
170
|
+
return { removed: false, reason: "hook block present but not in the expected shape; remove it manually" };
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(zshrcPath, cleaned, { encoding: "utf8", mode: 0o644 });
|
|
173
|
+
return { removed: true };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error && typeof error === "object" && (error as { code?: unknown }).code === "ENOENT") {
|
|
176
|
+
return { removed: false, reason: "not installed" };
|
|
177
|
+
}
|
|
178
|
+
return { removed: false, reason: "read/write failed" };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether a real `claude` executable is discoverable from this process's PATH. */
|
|
183
|
+
export function claudeCodeCliInstalled(pathValue = process.env.PATH): boolean {
|
|
184
|
+
if (!pathValue) return false;
|
|
185
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
186
|
+
// An empty PATH segment means the current directory. Do not let the proxy treat a
|
|
187
|
+
// workspace-local file as a durable user installation.
|
|
188
|
+
if (!directory) continue;
|
|
189
|
+
const candidate = join(directory, "claude");
|
|
190
|
+
try {
|
|
191
|
+
if (!statSync(candidate).isFile()) continue;
|
|
192
|
+
accessSync(candidate, constants.X_OK);
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
// Keep scanning PATH after missing, non-file, and non-executable entries.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Keep the shell hook aligned with the integration that can actually consume it.
|
|
203
|
+
* Claude Desktop uses its own profile and does not source `.zshrc`; this hook exists
|
|
204
|
+
* only for plain Claude Code CLI launches.
|
|
205
|
+
*
|
|
206
|
+
* Reconciliation is PATH-sensitive by construction: "Claude Code is installed" is answered
|
|
207
|
+
* from the PATH of whichever process calls this. A launchd/service context with a stripped
|
|
208
|
+
* PATH can therefore fail to see a `claude` the user's interactive shell finds, and this will
|
|
209
|
+
* remove the hook. That is the intended failure direction — removing an OpenCodex-owned block
|
|
210
|
+
* is reversible on the next foreground `ocx start`, whereas leaving a hook pointing at an
|
|
211
|
+
* uninstalled CLI is the stale state this reconciliation exists to clear. Only the block
|
|
212
|
+
* carrying our own marker is ever touched; user lines are preserved.
|
|
213
|
+
*/
|
|
214
|
+
export function reconcileShellHook(systemEnvInjected: boolean): {
|
|
215
|
+
changed: boolean;
|
|
216
|
+
state: "installed" | "absent" | "failed";
|
|
217
|
+
reason?: string;
|
|
218
|
+
} {
|
|
219
|
+
if (process.platform !== "darwin") return { changed: false, state: "absent", reason: "not macOS" };
|
|
220
|
+
if (systemEnvInjected && claudeCodeCliInstalled()) {
|
|
221
|
+
const result = installShellHook();
|
|
222
|
+
if (result.installed) return { changed: true, state: "installed" };
|
|
223
|
+
if (result.reason === "already installed") {
|
|
224
|
+
return { changed: false, state: "installed", reason: result.reason };
|
|
225
|
+
}
|
|
226
|
+
return { changed: false, state: "failed", reason: result.reason ?? "install failed" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = uninstallShellHook();
|
|
230
|
+
if (!result.removed && result.reason !== "not installed") {
|
|
231
|
+
return { changed: false, state: "failed", reason: result.reason ?? "remove failed" };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
changed: result.removed,
|
|
235
|
+
state: "absent",
|
|
236
|
+
reason: systemEnvInjected ? "Claude Code not installed" : "system environment inactive",
|
|
237
|
+
};
|
|
238
|
+
}
|