@bitkyc08/opencodex 2.57.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 +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- 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 +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- 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 +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- 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/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- 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 +72 -1
- package/src/cli/uninstall-client-state.ts +12 -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 +58 -20
- 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/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -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/native-profile-startup.ts +64 -20
- 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/atomic-write.ts +83 -8
- 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 +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- 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/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- 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/derive.ts +6 -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-discovery.ts +19 -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-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- 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/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -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/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- 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/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -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 +23 -2
- 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/stop-teardown.ts +8 -1
- 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/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- 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 +104 -3
- package/src/types/provider.ts +32 -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-Cz7CLdif.js +0 -128
|
@@ -925,12 +925,32 @@ async function poolSetting(
|
|
|
925
925
|
if (response.status !== 200) return apiError(response.json, `failed to read ${label}`, response.status);
|
|
926
926
|
const strategy = response.json[transport.strategyKey];
|
|
927
927
|
const sticky = response.json[transport.stickyKey];
|
|
928
|
+
const autoSwitchThreshold = typeof response.json.autoSwitchThreshold === "number"
|
|
929
|
+
? response.json.autoSwitchThreshold
|
|
930
|
+
: undefined;
|
|
928
931
|
if (wantsJson) {
|
|
929
932
|
// Pool-neutral key names: the two routes spell the same two settings differently, and a
|
|
930
933
|
// `--json` consumer should not have to branch on which pool answered.
|
|
931
|
-
|
|
934
|
+
const payload: Record<string, unknown> = { ok: true, provider: name, strategy, stickyLimit: sticky };
|
|
935
|
+
if (autoSwitchThreshold !== undefined) {
|
|
936
|
+
payload.autoSwitchThreshold = autoSwitchThreshold;
|
|
937
|
+
}
|
|
938
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
932
939
|
} else {
|
|
933
|
-
|
|
940
|
+
if (field === "strategy" && autoSwitchThreshold !== undefined) {
|
|
941
|
+
const thresholdSummary = strategy === "round-robin"
|
|
942
|
+
? "threshold not used"
|
|
943
|
+
: autoSwitchThreshold > 0
|
|
944
|
+
? (strategy === "fill-first"
|
|
945
|
+
? `drain at ${autoSwitchThreshold}%`
|
|
946
|
+
: strategy === "reset-first"
|
|
947
|
+
? `nearest reset below ${autoSwitchThreshold}%`
|
|
948
|
+
: `switch at ${autoSwitchThreshold}%`)
|
|
949
|
+
: "proactive switching off";
|
|
950
|
+
console.log(`${name}: ${label} is ${String(strategy)} (${thresholdSummary})`);
|
|
951
|
+
} else {
|
|
952
|
+
console.log(`${name}: ${label} is ${String(field === "strategy" ? strategy : sticky)}`);
|
|
953
|
+
}
|
|
934
954
|
}
|
|
935
955
|
return 0;
|
|
936
956
|
}
|
|
@@ -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 ? {
|
|
@@ -4,7 +4,7 @@ import { getConfigPath, mutatePersistedConfig, readConfigDiagnostics, sanitizeMo
|
|
|
4
4
|
import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning-effort";
|
|
5
5
|
import type { OcxConfig } from "../types";
|
|
6
6
|
import { normalizeVisionReasoningForModel } from "../vision/reasoning";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ServiceApiTokenState } from "../lib/service-secrets";
|
|
8
8
|
import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api";
|
|
9
9
|
|
|
10
10
|
const USAGE = `Usage:
|
|
@@ -38,9 +38,8 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
38
38
|
* `client` block, which is the same defect in miniature: the presence of configuration is not
|
|
39
39
|
* evidence that the connection works, and a machine whose data-plane token was revoked, rotated
|
|
40
40
|
* away or deleted would have been labelled `connected: true` while it could not reach the hub at
|
|
41
|
-
* all.
|
|
42
|
-
*
|
|
43
|
-
* pure and testable.
|
|
41
|
+
* all. The read-only projection compares the bounded token file's fingerprint against the
|
|
42
|
+
* connection record, and passes that answer in so this formatter stays pure and testable.
|
|
44
43
|
*
|
|
45
44
|
* Synthetic and NOT persisted, for two reasons. `clientConnectionSchema` is `.strict()`, so a
|
|
46
45
|
* `client.note` field would not validate; and persisted prose drifts from the behaviour it
|
|
@@ -49,7 +48,7 @@ const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
|
49
48
|
*/
|
|
50
49
|
export function remoteHubConfigNote(
|
|
51
50
|
config: OcxConfig,
|
|
52
|
-
readConnection: () =>
|
|
51
|
+
readConnection: () => RemoteHubConnectionObservation,
|
|
53
52
|
): { connected: boolean; origin: string; note: string } | null {
|
|
54
53
|
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
55
54
|
// A thunk, so a standalone or hub install pays nothing: the guard above returns first and the
|
|
@@ -67,6 +66,36 @@ export function remoteHubConfigNote(
|
|
|
67
66
|
return { connected, origin: config.client.serverUrl, note };
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
export type RemoteHubConnectionObservation = {
|
|
70
|
+
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
71
|
+
reason?: string;
|
|
72
|
+
token: "owned" | "missing" | "changed" | "unsafe";
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function remoteHubConnectionFromTokenState(
|
|
76
|
+
config: Pick<OcxConfig, "client">,
|
|
77
|
+
tokenState: ServiceApiTokenState,
|
|
78
|
+
): RemoteHubConnectionObservation {
|
|
79
|
+
const token = tokenState.kind === "absent"
|
|
80
|
+
? "missing"
|
|
81
|
+
: tokenState.kind === "unsafe"
|
|
82
|
+
? "unsafe"
|
|
83
|
+
: tokenState.fingerprint === config.client?.tokenFingerprint ? "owned" : "changed";
|
|
84
|
+
return { state: "connected", token };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readRemoteHubConfigNote(config: OcxConfig): Promise<ReturnType<typeof remoteHubConfigNote>> {
|
|
88
|
+
if (config.runtimeRole !== "client" || !config.client) return null;
|
|
89
|
+
// This display command needs only connection ownership, not lifecycle recovery, catalog
|
|
90
|
+
// readiness, or any write-capable connect machinery. Keep the read on the bounded token
|
|
91
|
+
// observer so a cold `config show` never imports the full connect command graph.
|
|
92
|
+
const { readServiceApiTokenState } = await import("../lib/service-secrets");
|
|
93
|
+
return remoteHubConfigNote(
|
|
94
|
+
config,
|
|
95
|
+
() => remoteHubConnectionFromTokenState(config, readServiceApiTokenState()),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
function redact(value: unknown, key = ""): unknown {
|
|
71
100
|
if (SECRET_KEYS.test(key) && typeof value === "string") return value ? "********" : value;
|
|
72
101
|
// `client.priorCatalog` is the base64 catalog snapshot connect took before overwriting the
|
|
@@ -168,19 +197,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
168
197
|
rejectArgs(args, USAGE);
|
|
169
198
|
const diagnostics = readConfigDiagnostics();
|
|
170
199
|
const redacted = redact(diagnostics.config);
|
|
171
|
-
|
|
172
|
-
// in, and `ocx config get/set` has no use for it.
|
|
173
|
-
const { collectClientConnectionStatus } = await import("./connect");
|
|
174
|
-
// The readiness probe is declined explicitly. `collectClientConnectionStatus` observes the
|
|
175
|
-
// local Codex ladder for a connected client, and observing it spawns `codex debug models`
|
|
176
|
-
// under a 45s budget. `ocx config show` reads only `state`, `reason` and `token` from the
|
|
177
|
-
// result, so paying for a subprocess here would buy nothing and would quietly turn a
|
|
178
|
-
// read-only config dump into a runtime probe. Returning no ladder resolves readiness to
|
|
179
|
-
// `unverified`, which is the honest answer for a caller that never asked.
|
|
180
|
-
const note = remoteHubConfigNote(
|
|
181
|
-
diagnostics.config,
|
|
182
|
-
() => collectClientConnectionStatus(undefined, undefined, { supportedEfforts: () => null }),
|
|
183
|
-
);
|
|
200
|
+
const note = await readRemoteHubConfigNote(diagnostics.config);
|
|
184
201
|
// First key, not last: it has to be read before the empty `providers` map that misled a
|
|
185
202
|
// reader into concluding nothing was configured anywhere.
|
|
186
203
|
const config = note && redacted && typeof redacted === "object" && !Array.isArray(redacted)
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "../codex/desired-state";
|
|
25
25
|
import { syncModelsToCodex } from "../codex/sync";
|
|
26
26
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
27
|
-
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
27
|
+
import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
|
|
28
28
|
import { stripGrokConfig } from "../grok/inject";
|
|
29
29
|
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
|
|
30
30
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
@@ -106,6 +106,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
106
106
|
restore: async deps => {
|
|
107
107
|
const restoreArgs = deps.args.slice(1);
|
|
108
108
|
const restoreJson = takeFlag(restoreArgs, "--json");
|
|
109
|
+
const removeProviderTable = takeFlag(restoreArgs, "--remove-codex-provider-table");
|
|
109
110
|
if (restoreArgs[0] === "back") {
|
|
110
111
|
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
111
112
|
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
@@ -146,6 +147,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
146
147
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
147
148
|
return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
|
|
148
149
|
}
|
|
150
|
+
if (removeProviderTable && !restoreJson) {
|
|
151
|
+
console.log("⚠️ Removing [model_providers.opencodex] means conversations already tagged opencodex will stop opening.");
|
|
152
|
+
}
|
|
149
153
|
const desired = setIntegrationEnabled("codex", false);
|
|
150
154
|
if (!desired.ok) {
|
|
151
155
|
if (restoreJson) {
|
|
@@ -191,9 +195,9 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
191
195
|
return grokCode;
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
|
-
let r:
|
|
198
|
+
let r: CodexNativeRestoreResult | Pick<CodexNativeRestoreResult, "success" | "message">;
|
|
195
199
|
try {
|
|
196
|
-
r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
200
|
+
r = await restoreNativeCodexAsync({ revalidateDesiredState: true, removeProviderTable });
|
|
197
201
|
} catch (err) {
|
|
198
202
|
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
199
203
|
}
|
|
@@ -232,7 +236,16 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
232
236
|
code = 1;
|
|
233
237
|
}
|
|
234
238
|
if (r.success) {
|
|
235
|
-
|
|
239
|
+
const retained = "retainedCodexProviderTable" in r ? r.retainedCodexProviderTable : undefined;
|
|
240
|
+
if (retained) {
|
|
241
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively.");
|
|
242
|
+
console.log("The following lines remain in $CODEX_HOME/config.toml because conversations already tagged opencodex resolve their provider only through this table:");
|
|
243
|
+
console.log(retained.lines.join("\n"));
|
|
244
|
+
console.log(`Follow-up: ${retained.followUp}`);
|
|
245
|
+
console.log("Switch back with: ocx restore back");
|
|
246
|
+
} else {
|
|
247
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
248
|
+
}
|
|
236
249
|
console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
|
|
237
250
|
} else {
|
|
238
251
|
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
@@ -952,6 +965,60 @@ export function decideStartWithLiveOwner(input: {
|
|
|
952
965
|
return input.ocxService === "1" ? "service-stay-out" : "refuse";
|
|
953
966
|
}
|
|
954
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
|
+
|
|
955
1022
|
export function resolveDispatchCommand(command: string | undefined): string | undefined {
|
|
956
1023
|
if (command === undefined) return undefined;
|
|
957
1024
|
if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command;
|