@bitkyc08/opencodex 2.58.0 → 2.60.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-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- 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 +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- 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 +26 -9
- 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 +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -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/opencode-go-additional-tools.ts +12 -2
- 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 +28 -35
- 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 +26 -6
- 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 +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- 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 +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- 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 +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- 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/internal/catalog-writer.ts +33 -1
- 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-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -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 +289 -16
- 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 +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -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 +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- 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 +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- 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 +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -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/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- 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/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
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
|
@@ -945,9 +945,9 @@ export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling";
|
|
|
945
945
|
*
|
|
946
946
|
* The #3106 guard exists so a bare `start` cannot shadow a healthy configured-port
|
|
947
947
|
* proxy with an ephemeral-port copy. An interactive `--port X` naming a DIFFERENT
|
|
948
|
-
* port than the live proxy's is an explicit sibling request, not that shadow
|
|
949
|
-
*
|
|
950
|
-
*
|
|
948
|
+
* port than the live proxy's is an explicit sibling request, not that shadow. The
|
|
949
|
+
* state-directory spend-ledger lease makes the final same-home refusal; keeping this
|
|
950
|
+
* decision allows isolated homes on one machine to remain independent.
|
|
951
951
|
* The service wrapper always passes the configured port and keeps its exact
|
|
952
952
|
* stay-out-of-the-way semantics: it never takes the sibling path.
|
|
953
953
|
*/
|
|
@@ -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;
|
package/src/cli/doctor.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
|
|
14
14
|
import { readPid } from "../config/process-state";
|
|
15
15
|
import { probeUncleanExitState } from "./status";
|
|
16
|
-
import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
16
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
17
17
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
18
18
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
19
19
|
import { maskAccountId } from "../lib/privacy";
|
|
@@ -26,7 +26,11 @@ import { withNativeMainSharedClaim } from "../codex/native-main-claim";
|
|
|
26
26
|
import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "../codex/native-profile-store";
|
|
27
27
|
import { NativeProfileError } from "../codex/native-profile-types";
|
|
28
28
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
scanCodexAgentRolesWithTomlModelFallback,
|
|
31
|
+
scanOpencodexDerivedCodexAgentRolesWithoutModelPin,
|
|
32
|
+
} from "../codex/subagent-model-fallback";
|
|
33
|
+
import { readCatalog, readCodexCatalogPath, readConfiguredDefaultModel } from "../codex/catalog/parsing";
|
|
30
34
|
import { diagnoseCodexShim, findCodexOnPath, isWindowsInteropDir, type CodexShimDiagnostic } from "../codex/shim";
|
|
31
35
|
import { providerTableString, rootTomlString } from "../codex/injected-marker";
|
|
32
36
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
@@ -1040,6 +1044,161 @@ export function chatgptPublicEndpointHint(
|
|
|
1040
1044
|
return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier.";
|
|
1041
1045
|
}
|
|
1042
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
* Bound for the doctor-side `/v1/models` read (#4646). A diagnostic must not hang on a proxy
|
|
1049
|
+
* that is listening but wedged mid-gather; when the read does not land in time the on-disk
|
|
1050
|
+
* catalog answers instead, and if that is unreadable too the verdict is "could not determine"
|
|
1051
|
+
* rather than a guess.
|
|
1052
|
+
*/
|
|
1053
|
+
const EXPOSED_MODELS_TIMEOUT_MS = 8000;
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Whether Codex's pinned default model is one this proxy exposes (#4646).
|
|
1057
|
+
*
|
|
1058
|
+
* Three states, not two. Reporting "not exposed" when the exposed set could not be read would
|
|
1059
|
+
* be a fabricated failure on exactly the installs least able to check it (proxy down, catalog
|
|
1060
|
+
* never synced), so an unreadable set is its own verdict.
|
|
1061
|
+
*/
|
|
1062
|
+
export type DefaultModelExposureStatus = "not_configured" | "exposed" | "not_exposed" | "undeterminable";
|
|
1063
|
+
|
|
1064
|
+
export interface DefaultModelExposure {
|
|
1065
|
+
status: DefaultModelExposureStatus;
|
|
1066
|
+
/** The configured pin, or null when Codex's config.toml has no root `model`. */
|
|
1067
|
+
model: string | null;
|
|
1068
|
+
/** Which surface answered; null when neither could be read. */
|
|
1069
|
+
source: "proxy" | "catalog" | null;
|
|
1070
|
+
detail: string;
|
|
1071
|
+
action?: string;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Exactly the catalog's own `RawEntry` shape, so an on-disk row needs no conversion. */
|
|
1075
|
+
type CatalogVisibilityRow = Record<string, unknown>;
|
|
1076
|
+
|
|
1077
|
+
export interface DefaultModelExposureDeps {
|
|
1078
|
+
readConfiguredModelFn?: () => string | null;
|
|
1079
|
+
/** The live proxy doctor already resolved, or null/absent when none is running. */
|
|
1080
|
+
live?: LiveProxy | null;
|
|
1081
|
+
fetchFn?: typeof fetch;
|
|
1082
|
+
readCatalogModelsFn?: () => readonly CatalogVisibilityRow[] | null;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Ids the running proxy advertises, or null when the read did not produce a usable answer.
|
|
1087
|
+
*
|
|
1088
|
+
* Null is deliberately indistinguishable across transport failure, a non-200, and a malformed
|
|
1089
|
+
* body, because every one of them means the same thing to the caller: this surface did not
|
|
1090
|
+
* answer, ask the next one. The 401 case is real rather than theoretical — `/v1/models` requires
|
|
1091
|
+
* data-plane admission on a non-loopback bind (`isApiAuthRequired`), and doctor deliberately
|
|
1092
|
+
* holds no data-plane key, so a remote-bound proxy always falls through to the catalog.
|
|
1093
|
+
*/
|
|
1094
|
+
async function fetchExposedModelIds(live: LiveProxy, fetchFn: typeof fetch): Promise<Set<string> | null> {
|
|
1095
|
+
try {
|
|
1096
|
+
const res = await fetchFn(`http://${probeHostname(live.hostname)}:${live.port}/v1/models`, {
|
|
1097
|
+
signal: AbortSignal.timeout(EXPOSED_MODELS_TIMEOUT_MS),
|
|
1098
|
+
});
|
|
1099
|
+
if (!res.ok) return null;
|
|
1100
|
+
const body = await res.json() as { data?: unknown };
|
|
1101
|
+
if (!Array.isArray(body?.data)) return null;
|
|
1102
|
+
const ids = new Set<string>();
|
|
1103
|
+
for (const row of body.data) {
|
|
1104
|
+
const id = (row as { id?: unknown } | null)?.id;
|
|
1105
|
+
if (typeof id === "string" && id.length > 0) ids.add(id);
|
|
1106
|
+
}
|
|
1107
|
+
return ids;
|
|
1108
|
+
} catch {
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** Picker-visible catalog slugs, or null when the catalog is absent or unparseable. */
|
|
1114
|
+
function catalogExposedModelIds(rows: readonly CatalogVisibilityRow[] | null): Set<string> | null {
|
|
1115
|
+
if (rows === null) return null;
|
|
1116
|
+
const ids = new Set<string>();
|
|
1117
|
+
for (const row of rows) {
|
|
1118
|
+
// `visibility: "hide"` rows are retained on purpose (see the native-toggle contract in
|
|
1119
|
+
// structure/catalog.md); they are exactly the rows a pin must not resolve to.
|
|
1120
|
+
if (!row || row.visibility !== "list") continue;
|
|
1121
|
+
const slug = row.slug;
|
|
1122
|
+
if (typeof slug === "string" && slug.length > 0) ids.add(slug);
|
|
1123
|
+
}
|
|
1124
|
+
return ids;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function defaultCatalogModels(): readonly CatalogVisibilityRow[] | null {
|
|
1128
|
+
const models = readCatalog(readCodexCatalogPath())?.models;
|
|
1129
|
+
return Array.isArray(models) ? models : null;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Compare Codex's root `model` pin against the models this install actually exposes (#4646).
|
|
1134
|
+
*
|
|
1135
|
+
* The exposed set is read, never recomputed. Reproducing the live assembly in the CLI would mean
|
|
1136
|
+
* duplicating an entitlements snapshot, a provider gather and account-selector expansion, and the
|
|
1137
|
+
* duplicate would drift — the same failure `formatStartupRoutingDetail` and `computeVersionSkew`
|
|
1138
|
+
* were extracted to prevent. So the running proxy answers when there is one, the on-disk catalog
|
|
1139
|
+
* answers otherwise, and neither is reconstructed here.
|
|
1140
|
+
*
|
|
1141
|
+
* Both surfaces are consulted before any negative verdict. They name a routed row through the
|
|
1142
|
+
* same `<provider>/<id>` slug space, but they are built by different code at different times, so
|
|
1143
|
+
* requiring both to disagree is what keeps an encoding or staleness difference from being
|
|
1144
|
+
* reported to the operator as a broken pin.
|
|
1145
|
+
*/
|
|
1146
|
+
export async function collectDefaultModelExposure(
|
|
1147
|
+
deps: DefaultModelExposureDeps = {},
|
|
1148
|
+
): Promise<DefaultModelExposure> {
|
|
1149
|
+
const configured = (deps.readConfiguredModelFn ?? readConfiguredDefaultModel)();
|
|
1150
|
+
const model = typeof configured === "string" ? configured.trim() : "";
|
|
1151
|
+
if (!model) {
|
|
1152
|
+
return {
|
|
1153
|
+
status: "not_configured",
|
|
1154
|
+
model: null,
|
|
1155
|
+
source: null,
|
|
1156
|
+
detail: "Codex config.toml pins no root `model`, so Codex picks from the exposed catalog",
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const live = deps.live ?? null;
|
|
1161
|
+
const proxyIds = live ? await fetchExposedModelIds(live, deps.fetchFn ?? fetch) : null;
|
|
1162
|
+
const catalogIds = catalogExposedModelIds((deps.readCatalogModelsFn ?? defaultCatalogModels)());
|
|
1163
|
+
if (proxyIds === null && catalogIds === null) {
|
|
1164
|
+
return {
|
|
1165
|
+
status: "undeterminable",
|
|
1166
|
+
model,
|
|
1167
|
+
source: null,
|
|
1168
|
+
detail: `could not read the exposed model set, so Codex \`model = "${model}"\` was not checked`,
|
|
1169
|
+
action: "Start the proxy with 'ocx start', or run 'ocx sync' to write the Codex catalog, then re-run 'ocx doctor'",
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const source = proxyIds !== null ? "proxy" as const : "catalog" as const;
|
|
1174
|
+
// `source` reports which surface produced the verdict, so a match names the surface that
|
|
1175
|
+
// matched rather than the one we happened to read first.
|
|
1176
|
+
const matched = proxyIds?.has(model) === true
|
|
1177
|
+
? "proxy" as const
|
|
1178
|
+
: catalogIds?.has(model) === true ? "catalog" as const : null;
|
|
1179
|
+
if (matched !== null) {
|
|
1180
|
+
return {
|
|
1181
|
+
status: "exposed",
|
|
1182
|
+
model,
|
|
1183
|
+
source: matched,
|
|
1184
|
+
detail: `Codex \`model = "${model}"\` is exposed by this install`,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
// Name only the surfaces that actually answered: claiming a check that did not happen is the
|
|
1188
|
+
// same defect as claiming an exposure verdict we could not reach.
|
|
1189
|
+
const checked = [
|
|
1190
|
+
...(proxyIds !== null ? ["the running proxy's /v1/models"] : []),
|
|
1191
|
+
...(catalogIds !== null ? ["the on-disk Codex catalog"] : []),
|
|
1192
|
+
].join(" and ");
|
|
1193
|
+
return {
|
|
1194
|
+
status: "not_exposed",
|
|
1195
|
+
model,
|
|
1196
|
+
source,
|
|
1197
|
+
detail: `Codex \`model = "${model}"\` is NOT exposed by this install (checked ${checked}), so every new Codex session starts on a model this proxy does not serve`,
|
|
1198
|
+
action: "Expose that model (enable it in the dashboard or drop it from 'disabledModels') and run 'ocx sync', or pin an exposed id as 'model' in CODEX_HOME/config.toml",
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1043
1202
|
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
1044
1203
|
if (args.includes("--fix-codex-runtime")) {
|
|
1045
1204
|
const resolved = resolveCodexRuntime();
|
|
@@ -1325,14 +1484,63 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1325
1484
|
console.log(line);
|
|
1326
1485
|
}
|
|
1327
1486
|
|
|
1487
|
+
// Adjacent to the section above because both read Codex's config.toml, and an operator
|
|
1488
|
+
// debugging "Codex config" wants the pinned model checked in the same place.
|
|
1489
|
+
console.log("\nCodex default model exposure");
|
|
1490
|
+
const defaultModelExposure = await collectDefaultModelExposure({ live });
|
|
1491
|
+
if (defaultModelExposure.status === "not_exposed") {
|
|
1492
|
+
console.log(` !! ${defaultModelExposure.detail}`);
|
|
1493
|
+
console.log(` Action: ${defaultModelExposure.action}`);
|
|
1494
|
+
} else if (defaultModelExposure.status === "undeterminable") {
|
|
1495
|
+
// Not `!!`: nothing is known to be wrong. The one thing this must never do is report an
|
|
1496
|
+
// unread set as a broken pin.
|
|
1497
|
+
console.log(` -- ${defaultModelExposure.detail}`);
|
|
1498
|
+
console.log(` Action: ${defaultModelExposure.action}`);
|
|
1499
|
+
} else {
|
|
1500
|
+
console.log(` ok ${defaultModelExposure.detail}`);
|
|
1501
|
+
}
|
|
1502
|
+
// Deliberately no `recordDoctorFailure()` and no `process.exitCode` write. A pin that is not
|
|
1503
|
+
// exposed is a degraded install, not an unusable one — the operator can still pick another
|
|
1504
|
+
// model in the session — and the rule above reserves FAIL for an unusable surface so a warning
|
|
1505
|
+
// cannot break a legitimately green pipeline.
|
|
1506
|
+
|
|
1328
1507
|
console.log("\nCodex agent role files");
|
|
1329
|
-
|
|
1330
|
-
|
|
1508
|
+
let roleScanError: unknown;
|
|
1509
|
+
const tomlFallbackRoles = scanCodexAgentRolesWithTomlModelFallback(
|
|
1510
|
+
resolveCodexHomeDirImpl(),
|
|
1511
|
+
cause => {
|
|
1512
|
+
roleScanError = cause;
|
|
1513
|
+
},
|
|
1514
|
+
);
|
|
1515
|
+
if (roleScanError) {
|
|
1516
|
+
console.log(` [WARN] unable to scan $CODEX_HOME/agents/*.toml: ${String(roleScanError)}`);
|
|
1517
|
+
} else if (tomlFallbackRoles.length === 0) {
|
|
1331
1518
|
console.log(" ok no per-role model_fallback fields in $CODEX_HOME/agents/*.toml");
|
|
1332
1519
|
} else {
|
|
1333
1520
|
console.log(` [WARN] ${tomlFallbackRoles.length} agent role file${tomlFallbackRoles.length === 1 ? "" : "s"} contain${tomlFallbackRoles.length === 1 ? "s" : ""} \`model_fallback\`: ${tomlFallbackRoles.join(", ")}`);
|
|
1334
1521
|
console.log(" Codex >= 0.146 rejects that field as unknown and skips the whole role. Move the chains to opencodex config `subagentModelFallbackByModel` (keyed by primary model) and remove the field from the TOML files.");
|
|
1335
1522
|
}
|
|
1523
|
+
// opencodex does not write these files; the Codex desktop external-agent import does, and it
|
|
1524
|
+
// drops the model pin on the way in. Observe-only: doctor never repairs or removes them.
|
|
1525
|
+
// Both role scans share the same directory listing, so a failure above already reported the
|
|
1526
|
+
// cause; skip the second scan rather than warn twice or print a false "ok".
|
|
1527
|
+
if (!roleScanError) {
|
|
1528
|
+
const unpinnedDerivedRoles = scanOpencodexDerivedCodexAgentRolesWithoutModelPin(
|
|
1529
|
+
resolveCodexHomeDirImpl(),
|
|
1530
|
+
cause => {
|
|
1531
|
+
roleScanError = cause;
|
|
1532
|
+
},
|
|
1533
|
+
);
|
|
1534
|
+
if (roleScanError) {
|
|
1535
|
+
console.log(` [WARN] unable to scan $CODEX_HOME/agents/*.toml: ${String(roleScanError)}`);
|
|
1536
|
+
} else if (unpinnedDerivedRoles.length === 0) {
|
|
1537
|
+
console.log(" ok every opencodex-derived role file in $CODEX_HOME/agents/*.toml pins a model");
|
|
1538
|
+
} else {
|
|
1539
|
+
console.log(` [WARN] ${unpinnedDerivedRoles.length} opencodex-derived role file${unpinnedDerivedRoles.length === 1 ? "" : "s"} without a \`model\` pin: ${unpinnedDerivedRoles.map(role => `${role}.toml`).join(", ")}`);
|
|
1540
|
+
console.log(" Codex runs these roles on the parent model, so a spawn records one role and another model. The `ocx-route` directive in the file cannot pin them: it is honoured only on the Claude Code `/v1/messages` path and is inert on `/v1/responses`.");
|
|
1541
|
+
console.log(" Add `model = \"<id>\"` to each file, or remove them. They usually come from the Codex desktop external-agent import of ~/.claude/agents/ocx-*.md; set `[desktop] external-agent-import-sync-item-types` with `SUBAGENTS = false` to stop it recreating them.");
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1336
1544
|
|
|
1337
1545
|
const dual = collectWslDualInstall();
|
|
1338
1546
|
if (dual.wsl) {
|
|
@@ -1397,6 +1605,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1397
1605
|
hints.push(`${row.detail}. Set ${row.envName} in the shell that starts the proxy, or store a literal key in config (value hidden here).`);
|
|
1398
1606
|
}
|
|
1399
1607
|
if (codexEnvKeyReadiness) hints.push(`${codexEnvKeyReadiness.detail}. ${codexEnvKeyReadiness.action}.`);
|
|
1608
|
+
// Only the negative verdict becomes a hint. "Could not determine" is usually just a proxy that
|
|
1609
|
+
// is not running, which `proxyDownRestartHint` already reports; repeating it here would put a
|
|
1610
|
+
// second line in the hint list for one fact.
|
|
1611
|
+
if (defaultModelExposure.status === "not_exposed") {
|
|
1612
|
+
hints.push(`${defaultModelExposure.detail}. ${defaultModelExposure.action}.`);
|
|
1613
|
+
}
|
|
1400
1614
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
1401
1615
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
1402
1616
|
if (!startup.rebootSafe) {
|
package/src/cli/help.ts
CHANGED
|
@@ -27,7 +27,8 @@ export function printUsage(): void {
|
|
|
27
27
|
|
|
28
28
|
Usage:
|
|
29
29
|
ocx setup Interactive setup (alias: init)
|
|
30
|
-
ocx start [--port <port>]
|
|
30
|
+
ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]
|
|
31
|
+
Start the proxy; SOCKS5 defaults to 127.0.0.1:10808
|
|
31
32
|
ocx stop Stop the proxy AND restore native Codex (plain codex works again)
|
|
32
33
|
ocx restore Restore native Codex without stopping (alias: eject)
|
|
33
34
|
ocx restore back Re-point codex at the running proxy (undo restore)
|
|
@@ -102,6 +103,8 @@ Examples:
|
|
|
102
103
|
ocx init Set up provider and inject into Codex
|
|
103
104
|
ocx start Start on default port (10100)
|
|
104
105
|
ocx start --port 8080 Start on custom port
|
|
106
|
+
ocx start --socks5 Outbound via SOCKS5 at 127.0.0.1:10808 (saved)
|
|
107
|
+
ocx start --socks5-off Clear a saved SOCKS5 outbound proxy
|
|
105
108
|
ocx help service Show service command help
|
|
106
109
|
ocx help hub Explain the hub topology, token file, and invites
|
|
107
110
|
ocx sync Sync available models to Codex`);
|
package/src/cli/hub.ts
CHANGED
|
@@ -228,8 +228,9 @@ export function hubInviteCommand(
|
|
|
228
228
|
managementUrl: string,
|
|
229
229
|
clients: OcxConnectedClientId[],
|
|
230
230
|
): string {
|
|
231
|
-
const
|
|
232
|
-
|
|
231
|
+
const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
232
|
+
const clientsFlag = clients.length > 0 ? ` --clients ${shellQuote(clients.join(","))}` : "";
|
|
233
|
+
return `echo ${shellQuote(code)} | ocx connect ${shellQuote(dataUrl)} --management-url ${shellQuote(managementUrl)}${clientsFlag} --pairing-code-stdin`;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
async function runInvite(args: string[], deps: HubCommandDeps): Promise<number> {
|