@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,63 @@
|
|
|
1
|
+
import type { importOrcaAccounts } from "../codex/orca-import";
|
|
2
|
+
|
|
3
|
+
const USAGE = "Usage: ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]";
|
|
4
|
+
|
|
5
|
+
export interface OrcaImportCommandDeps {
|
|
6
|
+
importAccounts?: typeof importOrcaAccounts;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Local-only: never sends source paths or credentials to a management listener. */
|
|
10
|
+
export async function cmdOrcaImport(args: string[], deps: OrcaImportCommandDeps = {}): Promise<number> {
|
|
11
|
+
let sourceDir: string | undefined;
|
|
12
|
+
let registryPath: string | undefined;
|
|
13
|
+
let apply = false;
|
|
14
|
+
const wantsJson = args.includes("--json");
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
for (let index = 0; index < args.length; index++) {
|
|
17
|
+
const arg = args[index]!;
|
|
18
|
+
if (seen.has(arg) || !["--source", "--registry", "--apply", "--json"].includes(arg)) {
|
|
19
|
+
console.error(USAGE);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
seen.add(arg);
|
|
23
|
+
if (arg === "--source" || arg === "--registry") {
|
|
24
|
+
const value = args[++index];
|
|
25
|
+
if (!value || value.startsWith("--")) {
|
|
26
|
+
console.error(USAGE);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
if (arg === "--source") sourceDir = value;
|
|
30
|
+
else registryPath = value;
|
|
31
|
+
} else if (arg === "--apply") apply = true;
|
|
32
|
+
}
|
|
33
|
+
if (!sourceDir || !registryPath) {
|
|
34
|
+
console.error(USAGE);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const run = deps.importAccounts ?? (await import("../codex/orca-import")).importOrcaAccounts;
|
|
39
|
+
const result = await run({ sourceDir, registryPath, apply });
|
|
40
|
+
if (wantsJson) console.log(JSON.stringify({
|
|
41
|
+
mode: result.mode, discovered: result.discovered, eligible: result.eligible,
|
|
42
|
+
imported: result.imported, duplicates: result.duplicates, invalid: result.invalid,
|
|
43
|
+
invalidReasons: result.invalidReasons,
|
|
44
|
+
}));
|
|
45
|
+
else {
|
|
46
|
+
console.log(`Orca: ${result.discovered} discovered, ${result.eligible} eligible, ${result.imported} imported, ${result.duplicates} duplicates, ${result.invalid} invalid.`);
|
|
47
|
+
if (result.invalid > 0) {
|
|
48
|
+
const reasons = Object.entries(result.invalidReasons).map(([reason, count]) => `${reason}=${count}`).join(", ");
|
|
49
|
+
console.log(`Invalid reasons: ${reasons}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(apply
|
|
52
|
+
? "Start the proxy and use Refresh quotas in Codex Auth to validate new accounts. Orca retains refresh ownership."
|
|
53
|
+
: "Preview only. Stop the proxy before repeating with --apply. Orca authentication files stay read-only.");
|
|
54
|
+
}
|
|
55
|
+
return result.invalid > 0 && result.eligible === 0 ? 1 : 0;
|
|
56
|
+
} catch {
|
|
57
|
+
// Filesystem/JSON exceptions may contain source paths or token fragments.
|
|
58
|
+
const error = "Orca import failed. Check the source, destination config, and stopped proxy; no successful completion is confirmed.";
|
|
59
|
+
if (wantsJson) console.log(JSON.stringify({ error: "orca_import_failed" }));
|
|
60
|
+
else console.error(error);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/cli/account.ts
CHANGED
|
@@ -57,6 +57,7 @@ const ACCOUNT_USAGE = `Usage:
|
|
|
57
57
|
ocx account clear-cooldown <provider> <account-id|main> [--json]
|
|
58
58
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
59
59
|
ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
|
|
60
|
+
ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
|
|
60
61
|
ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
|
|
61
62
|
ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
|
|
62
63
|
ocx account cancel <provider> [--flow <flow-id>] [--json]
|
|
@@ -323,13 +324,36 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
323
324
|
if (res.status === 0) return proxyUnreachable(res.transportError);
|
|
324
325
|
if (res.status !== 200) return apiError(res.json, `failed to switch ${name}`, res.status);
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
327
|
+
// The route reports this only when routing would drop the pin it just recorded, so an
|
|
328
|
+
// absent field means the pin survives (#4521).
|
|
329
|
+
const pinDrainReason = typeof res.json.pinDrainReason === "string" ? res.json.pinDrainReason : undefined;
|
|
330
|
+
if (wantsJson) {
|
|
331
|
+
console.log(JSON.stringify({
|
|
332
|
+
ok: true,
|
|
333
|
+
provider: name,
|
|
334
|
+
type: c.type,
|
|
335
|
+
activeId,
|
|
336
|
+
...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
|
|
337
|
+
}, null, 2));
|
|
338
|
+
} else {
|
|
339
|
+
console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
|
|
340
|
+
}
|
|
328
341
|
if (c.type === "codex") {
|
|
329
342
|
console.error("Takes effect immediately; running threads move on their next request, and in-flight requests keep the account they captured.");
|
|
330
343
|
const active = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
const threshold = active.status === 200 && typeof active.json.autoSwitchThreshold === "number"
|
|
345
|
+
? active.json.autoSwitchThreshold
|
|
346
|
+
: undefined;
|
|
347
|
+
if (pinDrainReason !== undefined) {
|
|
348
|
+
// "may override" is the right caveat for a pin that is currently fine and could be
|
|
349
|
+
// overtaken later. It is the wrong sentence for one the next request will discard, and
|
|
350
|
+
// printing only that is what left the operator believing the account was pinned.
|
|
351
|
+
const because = pinDrainReason === "quota_threshold"
|
|
352
|
+
? `is at or above the auto-switch threshold${threshold !== undefined ? ` (${threshold}%)` : ""}`
|
|
353
|
+
: `cannot currently be selected (${pinDrainReason})`;
|
|
354
|
+
console.error(`Note: ${displayId(activeId)} ${because}, so routing releases this pin on its next request.`);
|
|
355
|
+
} else if (threshold !== undefined && threshold > 0) {
|
|
356
|
+
console.error(`Note: auto-switch (threshold ${threshold}%) may override this pin.`);
|
|
333
357
|
}
|
|
334
358
|
}
|
|
335
359
|
return 0;
|
|
@@ -360,6 +384,10 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
|
|
|
360
384
|
if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
|
|
361
385
|
if (sub === "add-key") return await cmdAddKey(rest, deps);
|
|
362
386
|
if (sub === "import") return await cmdImport(rest, deps);
|
|
387
|
+
if (sub === "import-orca") {
|
|
388
|
+
const { cmdOrcaImport } = await import("./account-orca-import");
|
|
389
|
+
return await cmdOrcaImport(rest);
|
|
390
|
+
}
|
|
363
391
|
if (sub === "main") {
|
|
364
392
|
const { cmdNativeMainAccount } = await import("./account-main");
|
|
365
393
|
return await cmdNativeMainAccount(rest, deps);
|
package/src/cli/capabilities.ts
CHANGED
|
@@ -360,6 +360,25 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
360
360
|
"`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL.",
|
|
361
361
|
],
|
|
362
362
|
},
|
|
363
|
+
{
|
|
364
|
+
command: ["account", "import-orca"],
|
|
365
|
+
summary: "Preview or register read-only links to Orca-managed Codex accounts without another login.",
|
|
366
|
+
routes: [],
|
|
367
|
+
flags: [
|
|
368
|
+
{ name: "--source", value: "string", required: true, summary: "Orca data directory containing codex-accounts." },
|
|
369
|
+
{ name: "--registry", value: "string", required: true, summary: "The chosen Orca profile's orca-data.json account registry." },
|
|
370
|
+
{ name: "--apply", value: "boolean", summary: "Register new accounts; requires a stopped proxy. Default is preview." },
|
|
371
|
+
{ name: "--json", value: "boolean", summary: "Emit counts and fixed invalid-reason codes without credentials or source paths." },
|
|
372
|
+
],
|
|
373
|
+
mutates: true,
|
|
374
|
+
json: "envelope",
|
|
375
|
+
details: [
|
|
376
|
+
"Local files only; never copies refresh tokens or changes Orca authentication files.",
|
|
377
|
+
"Skips existing ChatGPT identities. New accounts remain pending until dashboard validation.",
|
|
378
|
+
"Orca must keep the source login available and refreshed; a missing or expired source fails closed.",
|
|
379
|
+
"Mixed eligible and invalid entries exit successfully; an all-invalid result exits nonzero.",
|
|
380
|
+
],
|
|
381
|
+
},
|
|
363
382
|
{
|
|
364
383
|
command: ["account", "refresh"],
|
|
365
384
|
summary: "Refresh account quotas without model validation; pending Codex accounts require dashboard consent.",
|
|
@@ -708,6 +727,27 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
708
727
|
"Does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.",
|
|
709
728
|
],
|
|
710
729
|
},
|
|
730
|
+
{
|
|
731
|
+
command: ["system", "codex-cli-update", "attest"],
|
|
732
|
+
summary: "Observe the selected or explicitly named Windows npm Codex installation files without enabling updates.",
|
|
733
|
+
routes: [],
|
|
734
|
+
flags: [
|
|
735
|
+
{ name: "--candidate", value: "string", summary: "Absolute npm codex.cmd or package bin/codex.js path; all four paths are all-or-none." },
|
|
736
|
+
{ name: "--npm-prefix", value: "string", summary: "Absolute prefix containing node_modules/@openai/codex." },
|
|
737
|
+
{ name: "--npm-cli", value: "string", summary: "Absolute node_modules/npm/bin/npm-cli.js path." },
|
|
738
|
+
{ name: "--node", value: "string", summary: "Absolute node.exe path; observed, never executed." },
|
|
739
|
+
{ name: "--json", value: "boolean", summary: "Emit the path-free installation identity observation." },
|
|
740
|
+
],
|
|
741
|
+
mutates: false,
|
|
742
|
+
json: "envelope",
|
|
743
|
+
details: [
|
|
744
|
+
"Opt-in Windows x64 local-volume inspection using held native file handles; refuses reparse points, active writers and unsupported layouts.",
|
|
745
|
+
"Without explicit paths, the proof-bound launcher snapshot identifies the selected candidate: the configured CODEX_CLI_PATH or the first codex on the captured PATH, with an OpenCodex wrapper resolving to its codex.opencodex-real backing. Discovery only proposes paths; the held-handle observation remains the authority.",
|
|
746
|
+
"Success binds observed file identities and bytes, not selected-runtime admission or installer ownership.",
|
|
747
|
+
"selectionAttested, managed and applyAllowed remain false. The digest is an observation, not a durable update permit.",
|
|
748
|
+
"Does not run the named Codex/npm/Node files, query a registry, install software, control processes or persist state.",
|
|
749
|
+
],
|
|
750
|
+
},
|
|
711
751
|
{
|
|
712
752
|
command: ["system", "codex-restart"],
|
|
713
753
|
summary: "Restart the Codex desktop app and app-servers.",
|
package/src/cli/claude.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { loadConfig } from "../config";
|
|
12
12
|
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
13
13
|
import { CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
|
|
14
|
-
import { effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
14
|
+
import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
15
15
|
import { claudeConfigDir, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
|
|
16
16
|
import { commandInvocation } from "../lib/win-exec";
|
|
17
17
|
import { isProxyAdmissionSecret } from "../server/auth-cors";
|
|
@@ -357,6 +357,22 @@ export function buildClaudeEnv(
|
|
|
357
357
|
if (config.claudeCode?.alwaysEnableEffort === true) {
|
|
358
358
|
setDefault("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
|
|
359
359
|
}
|
|
360
|
+
// Tool-search deferral (#4838). Claude Code disables MCP tool deferral whenever
|
|
361
|
+
// ANTHROPIC_BASE_URL is not a first-party Anthropic host — keyed on the host, not
|
|
362
|
+
// the model — so every routed session inlines all MCP tool schemas. Its own log
|
|
363
|
+
// line states the precondition: "Set ENABLE_TOOL_SEARCH=true (or auto / auto:N)
|
|
364
|
+
// if your proxy forwards tool_reference blocks."
|
|
365
|
+
//
|
|
366
|
+
// We forward them on the native Anthropic passthrough route only. A translated
|
|
367
|
+
// route cannot honour the shape: deferred tools still carry input_schema on the
|
|
368
|
+
// wire (deferral is a server-side context optimisation, not a smaller request),
|
|
369
|
+
// toolsToResponses drops the tool_search server tool and ignores defer_loading,
|
|
370
|
+
// and compatibility.ts lists tool_search/tool_reference/deferred_tools as
|
|
371
|
+
// unsupported. Turning it on there leaves the routed provider holding every
|
|
372
|
+
// schema while Claude Code stops counting them and stops compacting, which is
|
|
373
|
+
// worse than the problem. So this stays opt-in per config rather than
|
|
374
|
+
// unconditional, and setDefault keeps an operator's own export.
|
|
375
|
+
setDefault("ENABLE_TOOL_SEARCH", claudeToolSearchEnv(config.claudeCode?.toolSearch));
|
|
360
376
|
// Context-window override: the official pair — MAX_CONTEXT_TOKENS alone is ignored
|
|
361
377
|
// for recognized claude-shaped ids unless DISABLE_COMPACT=1 rides along (devlog 135).
|
|
362
378
|
const maxCtx = config.claudeCode?.maxContextTokens;
|
|
@@ -542,6 +558,18 @@ export function claudeLaunchPreflight(
|
|
|
542
558
|
: { kind: "native", notice: CLAUDE_NATIVE_ROUTING_OFF };
|
|
543
559
|
}
|
|
544
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Levers a native (non-routed) launch must shed. The loop below deletes them
|
|
563
|
+
* unconditionally, because each one either points Claude Code at a gateway that
|
|
564
|
+
* is not there or asserts host ownership that a native session does not have.
|
|
565
|
+
*
|
|
566
|
+
* ENABLE_TOOL_SEARCH is deliberately NOT in this list (#4838). It is the only
|
|
567
|
+
* one of these whose value is meaningful to a native session: natively the base
|
|
568
|
+
* URL is first-party, where deferral is already Claude Code's default and the
|
|
569
|
+
* variable is the user's own tuning knob (auto:N, force). Stripping it would
|
|
570
|
+
* delete a preference that works, to protect a session that does not need
|
|
571
|
+
* protecting.
|
|
572
|
+
*/
|
|
545
573
|
const NATIVE_STRIPPED_LEVERS = [
|
|
546
574
|
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY",
|
|
547
575
|
"CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
|
|
@@ -3,18 +3,53 @@ import {
|
|
|
3
3
|
type CodexCliInstallProvenanceDeps,
|
|
4
4
|
type CodexCliInstallReport,
|
|
5
5
|
} from "../codex/cli-install-provenance";
|
|
6
|
+
import type {
|
|
7
|
+
CodexCliInstallationIdentityInput,
|
|
8
|
+
CodexCliInstallationIdentityReport,
|
|
9
|
+
} from "../codex/cli-installation-identity";
|
|
10
|
+
import type {
|
|
11
|
+
CodexCliInstallationSnapshot,
|
|
12
|
+
CodexCliInstallationTargetDerivation,
|
|
13
|
+
} from "../codex/cli-installation-targets";
|
|
6
14
|
import { CliUsageError, isJsonOption, printData, runCliAction } from "./runtime-api";
|
|
7
15
|
import { trustedNodeLauncherContext } from "./launcher-context";
|
|
8
16
|
|
|
9
17
|
export const CODEX_CLI_UPDATE_USAGE = `Usage:
|
|
10
|
-
ocx system codex-cli-update check [--json]
|
|
18
|
+
ocx system codex-cli-update check [--json]
|
|
19
|
+
ocx system codex-cli-update attest [--json]
|
|
20
|
+
ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]`;
|
|
11
21
|
|
|
12
22
|
export type ParsedCodexCliUpdateArgs = Readonly<{
|
|
13
23
|
json: boolean;
|
|
24
|
+
}> | Readonly<{
|
|
25
|
+
json: boolean;
|
|
26
|
+
attest: CodexCliInstallationIdentityInput | "selected";
|
|
14
27
|
}>;
|
|
15
28
|
|
|
16
29
|
export interface CodexCliUpdateCommandDeps {
|
|
17
30
|
readonly inspectInstall?: (deps: CodexCliInstallProvenanceDeps) => Promise<CodexCliInstallReport>;
|
|
31
|
+
readonly inspectIdentity?: (input: CodexCliInstallationIdentityInput) => Promise<CodexCliInstallationIdentityReport>;
|
|
32
|
+
readonly deriveInstallationInput?: (
|
|
33
|
+
snapshot: CodexCliInstallationSnapshot,
|
|
34
|
+
) => CodexCliInstallationTargetDerivation;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function identitySummary(report: CodexCliInstallationIdentityReport): string[] {
|
|
38
|
+
return [
|
|
39
|
+
`status: ${report.status}`,
|
|
40
|
+
`reason: ${report.reason}`,
|
|
41
|
+
`candidate-source: ${report.candidateSource}`,
|
|
42
|
+
`installation-identity-observed: ${report.installationIdentityObserved ? "yes" : "no"}`,
|
|
43
|
+
`selection-attested: ${report.selectionAttested ? "yes" : "no"}`,
|
|
44
|
+
`managed: ${report.managed ? "yes" : "no"}`,
|
|
45
|
+
`apply-allowed: ${report.applyAllowed ? "yes" : "no"}`,
|
|
46
|
+
`package-version: ${report.packageVersion ?? "unavailable"}`,
|
|
47
|
+
`npm-version: ${report.npmVersion ?? "unavailable"}`,
|
|
48
|
+
`identity-digest: ${report.identityDigest ?? "unavailable"}`,
|
|
49
|
+
`proof: ${report.proof ?? "unavailable"}`,
|
|
50
|
+
`toolchain: ${report.toolchain}`,
|
|
51
|
+
"scope: installation identity only; runtime selection and update ownership are not attested",
|
|
52
|
+
];
|
|
18
53
|
}
|
|
19
54
|
|
|
20
55
|
function installSummary(report: CodexCliInstallReport): string[] {
|
|
@@ -47,8 +82,38 @@ export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCli
|
|
|
47
82
|
}
|
|
48
83
|
positional.push(token);
|
|
49
84
|
}
|
|
85
|
+
if (positional[0] === "attest") {
|
|
86
|
+
// No options: attest the selected candidate identified from the proof-bound
|
|
87
|
+
// launcher snapshot. The four explicit paths remain all-or-none.
|
|
88
|
+
if (positional.length === 1) {
|
|
89
|
+
return Object.freeze({ json, attest: "selected" as const });
|
|
90
|
+
}
|
|
91
|
+
const options = new Map<string, keyof CodexCliInstallationIdentityInput>([
|
|
92
|
+
["--candidate", "candidate"], ["--npm-prefix", "npmPrefix"],
|
|
93
|
+
["--npm-cli", "npmCli"], ["--node", "node"],
|
|
94
|
+
]);
|
|
95
|
+
const input: Partial<Record<keyof CodexCliInstallationIdentityInput, string>> = {};
|
|
96
|
+
for (let index = 1; index < positional.length; index += 2) {
|
|
97
|
+
const key = options.get(positional[index]!);
|
|
98
|
+
if (!key || input[key] !== undefined) {
|
|
99
|
+
throw new CliUsageError("unsupported or duplicate attest option", CODEX_CLI_UPDATE_USAGE);
|
|
100
|
+
}
|
|
101
|
+
const value = positional[index + 1];
|
|
102
|
+
if (!value || !value.trim() || /[\0\r\n]/.test(value)
|
|
103
|
+
|| !(value.startsWith("/") || /^[a-z]:[\\/]/i.test(value))) {
|
|
104
|
+
throw new CliUsageError("attest options require explicit absolute paths", CODEX_CLI_UPDATE_USAGE);
|
|
105
|
+
}
|
|
106
|
+
input[key] = value;
|
|
107
|
+
}
|
|
108
|
+
if (!input.candidate || !input.npmPrefix || !input.npmCli || !input.node) {
|
|
109
|
+
throw new CliUsageError("attest requires --candidate, --npm-prefix, --npm-cli and --node", CODEX_CLI_UPDATE_USAGE);
|
|
110
|
+
}
|
|
111
|
+
return Object.freeze({ json, attest: Object.freeze({
|
|
112
|
+
candidate: input.candidate, npmPrefix: input.npmPrefix, npmCli: input.npmCli, node: input.node,
|
|
113
|
+
}) });
|
|
114
|
+
}
|
|
50
115
|
if (positional[0] !== "check") {
|
|
51
|
-
throw new CliUsageError("codex-cli-update action must be check", CODEX_CLI_UPDATE_USAGE);
|
|
116
|
+
throw new CliUsageError("codex-cli-update action must be check or attest", CODEX_CLI_UPDATE_USAGE);
|
|
52
117
|
}
|
|
53
118
|
if (positional.length > 1) {
|
|
54
119
|
throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
|
|
@@ -72,6 +137,36 @@ export async function handleCodexCliUpdateCommand(
|
|
|
72
137
|
throw error;
|
|
73
138
|
}
|
|
74
139
|
return runCliAction(async () => {
|
|
140
|
+
if ("attest" in parsed) {
|
|
141
|
+
let report: CodexCliInstallationIdentityReport;
|
|
142
|
+
try {
|
|
143
|
+
const identityModule = await import("../codex/cli-installation-identity");
|
|
144
|
+
const inspectIdentity = deps.inspectIdentity ?? identityModule.inspectCodexCliInstallationIdentity;
|
|
145
|
+
if (parsed.attest === "selected") {
|
|
146
|
+
// The proof-bound launcher snapshot is the only trusted source for the
|
|
147
|
+
// selected candidate; a direct Bun/source launch has none and refuses.
|
|
148
|
+
const snapshot = trustedNodeLauncherContext()?.codexCliInspectionEnv;
|
|
149
|
+
const derive = deps.deriveInstallationInput
|
|
150
|
+
?? (await import("../codex/cli-installation-targets")).deriveCodexCliInstallationInput;
|
|
151
|
+
const derived = derive({
|
|
152
|
+
codexCliPath: snapshot?.codexCliPath ?? null,
|
|
153
|
+
path: snapshot?.path ?? null,
|
|
154
|
+
pathExt: snapshot?.pathExt ?? null,
|
|
155
|
+
});
|
|
156
|
+
report = derived.kind === "derived"
|
|
157
|
+
? await inspectIdentity(derived.input)
|
|
158
|
+
: identityModule.codexCliInstallationRefusal(derived.reason, "selected");
|
|
159
|
+
} else {
|
|
160
|
+
report = await inspectIdentity(parsed.attest);
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Filesystem/native errors can contain the explicit private paths. The
|
|
164
|
+
// read-only inspector normally returns a refusal; unexpected errors stay redacted.
|
|
165
|
+
throw new Error("Installation identity inspection failed");
|
|
166
|
+
}
|
|
167
|
+
printData(report, parsed.json, identitySummary(report));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
75
170
|
const trustedInspectionEnv = trustedNodeLauncherContext()?.codexCliInspectionEnv;
|
|
76
171
|
const inspectionDeps: CodexCliInstallProvenanceDeps = trustedInspectionEnv
|
|
77
172
|
&& trustedInspectionEnv.managerRoots !== null ? {
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -965,6 +965,60 @@ export function decideStartWithLiveOwner(input: {
|
|
|
965
965
|
return input.ocxService === "1" ? "service-stay-out" : "refuse";
|
|
966
966
|
}
|
|
967
967
|
|
|
968
|
+
/** What `chooseListenPort` does when the preferred port stayed busy through prefer-retry. */
|
|
969
|
+
export type BusyPreferredPortDecision =
|
|
970
|
+
| "hop"
|
|
971
|
+
| "refuse-live-proxy"
|
|
972
|
+
| "service-stay-out"
|
|
973
|
+
| "refuse-unidentified-holder";
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Pure decision for a soft `start` whose preferred port is busy and whose only remaining
|
|
977
|
+
* option is an ephemeral port.
|
|
978
|
+
*
|
|
979
|
+
* The hop exists so a first start is not defeated by a port this machine happens to be
|
|
980
|
+
* using. What it must never be is a silent answer to "someone is already here": a start
|
|
981
|
+
* that hops takes over this home's pid and runtime-port records and re-points Codex at
|
|
982
|
+
* itself, so hopping past a live opencodex leaves two proxies running and the editor
|
|
983
|
+
* talking to the one the user did not mean (#5004). The hop path never asked who held the
|
|
984
|
+
* port, and `findLiveProxy` returning null — a stale record, a probe that lost a race, a
|
|
985
|
+
* loopback family split — was enough to reach it.
|
|
986
|
+
*
|
|
987
|
+
* So the decision is made from the holder's own answer rather than from this home's
|
|
988
|
+
* bookkeeping, and both outcomes stop the start. An opencodex answer is the duplicate this
|
|
989
|
+
* closes. A holder that does not answer as opencodex is deliberately NOT called foreign:
|
|
990
|
+
* an identity probe returns the same nothing for a foreign server, an unreachable one, and
|
|
991
|
+
* one that lost a race, so all the start can honestly say is that the port it was told to
|
|
992
|
+
* use is taken by something it could not identify — and moving to an arbitrary port is the
|
|
993
|
+
* one response that hides that from the user while re-pointing Codex. An explicit
|
|
994
|
+
* `--port` never reaches here (`findAvailablePort` refuses the fallback instead), and a
|
|
995
|
+
* configured port of 0 is a request for an ephemeral port, not a collision.
|
|
996
|
+
*
|
|
997
|
+
* Service-wrapper context keeps the semantics `decideStartWithLiveOwner` gives it: a
|
|
998
|
+
* healthy proxy on the port means the port is served, and the wrapper's
|
|
999
|
+
* `if %ERRORLEVEL% NEQ 0` loop must see a zero exit rather than respawn every 5 seconds.
|
|
1000
|
+
*/
|
|
1001
|
+
export function decideBusyPreferredPort(input: {
|
|
1002
|
+
preferredPort: number;
|
|
1003
|
+
selectedPort: number;
|
|
1004
|
+
hardPin: boolean;
|
|
1005
|
+
holderIsOpencodex: boolean;
|
|
1006
|
+
ocxService: string | undefined;
|
|
1007
|
+
}): BusyPreferredPortDecision {
|
|
1008
|
+
// Port 0 (or an unusable preference) asked the OS to choose; nothing was taken away.
|
|
1009
|
+
if (input.preferredPort <= 0) return "hop";
|
|
1010
|
+
// The preferred port was obtained — no hop happened, nothing to decide.
|
|
1011
|
+
if (input.selectedPort === input.preferredPort) return "hop";
|
|
1012
|
+
// Defensive: a hard pin cannot reach a different port, and if it ever did, the pin is
|
|
1013
|
+
// the user's explicit instruction and not something to answer with a refusal here.
|
|
1014
|
+
if (input.hardPin) return "hop";
|
|
1015
|
+
if (input.holderIsOpencodex) {
|
|
1016
|
+
// Same sentinel rule as decideStartWithLiveOwner: only the exact "1" is service context.
|
|
1017
|
+
return input.ocxService === "1" ? "service-stay-out" : "refuse-live-proxy";
|
|
1018
|
+
}
|
|
1019
|
+
return "refuse-unidentified-holder";
|
|
1020
|
+
}
|
|
1021
|
+
|
|
968
1022
|
export function resolveDispatchCommand(command: string | undefined): string | undefined {
|
|
969
1023
|
if (command === undefined) return undefined;
|
|
970
1024
|
if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command;
|