@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
4
|
+
import { SHIM_MARKER } from "./shim-templates";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A PATH entry that reaches Windows through WSL drive interop
|
|
8
|
+
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
9
|
+
* /etc/wsl.conf [automount] root).
|
|
10
|
+
*/
|
|
11
|
+
export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
|
|
12
|
+
const root = automountRoot.replace(/\/+$/, "");
|
|
13
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CodexPathScanDeps = {
|
|
18
|
+
pathValue?: string;
|
|
19
|
+
wsl?: boolean;
|
|
20
|
+
/** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
|
|
21
|
+
posixPaths?: boolean;
|
|
22
|
+
automountRoot?: string;
|
|
23
|
+
exists?: (path: string) => boolean;
|
|
24
|
+
isShimFile?: (path: string) => boolean;
|
|
25
|
+
isDirectory?: (path: string) => boolean;
|
|
26
|
+
realpath?: (path: string) => string;
|
|
27
|
+
executableEligible?: (path: string) => boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type CodexPathCandidate = {
|
|
31
|
+
path: string;
|
|
32
|
+
isShim: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function realIsDirectory(path: string): boolean {
|
|
36
|
+
try {
|
|
37
|
+
return lstatSync(path).isDirectory();
|
|
38
|
+
} catch {
|
|
39
|
+
return true; // unreadable -> treat as unusable
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SHIM_HEADER_MAX_BYTES = 16 * 1024;
|
|
44
|
+
|
|
45
|
+
/** Shared POSIX eligibility, following launcher symlinks without opening special files. */
|
|
46
|
+
export function isExecutableCodexCandidate(path: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
if (process.platform !== "win32") accessSync(path, constants.X_OK);
|
|
49
|
+
return statSync(path).isFile();
|
|
50
|
+
} catch { return false; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function inspectShimFile(path: string): boolean | null {
|
|
54
|
+
let fd: number | undefined;
|
|
55
|
+
try {
|
|
56
|
+
if (process.platform !== "win32") {
|
|
57
|
+
try { accessSync(path, constants.X_OK); } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
// Follow npm launcher symlinks, but never read a known special file.
|
|
60
|
+
if (!statSync(path).isFile()) return null;
|
|
61
|
+
const flags = constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NONBLOCK);
|
|
62
|
+
fd = openSync(path, flags);
|
|
63
|
+
// The entry can change after stat; a replacement FIFO must not block open or read.
|
|
64
|
+
if (!fstatSync(fd).isFile()) return null;
|
|
65
|
+
const header = Buffer.allocUnsafe(SHIM_HEADER_MAX_BYTES);
|
|
66
|
+
let total = 0;
|
|
67
|
+
while (total < header.length) {
|
|
68
|
+
const count = readSync(fd, header, total, header.length - total, total);
|
|
69
|
+
if (count === 0) break;
|
|
70
|
+
total += count;
|
|
71
|
+
}
|
|
72
|
+
return header.subarray(0, total).toString("utf8").includes(SHIM_MARKER);
|
|
73
|
+
} catch {
|
|
74
|
+
// An unreadable command still shadows later PATH entries; do not claim they are active.
|
|
75
|
+
return false;
|
|
76
|
+
} finally {
|
|
77
|
+
if (fd !== undefined) closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function commandNames(name: string): string[] {
|
|
82
|
+
if (process.platform !== "win32") return [name];
|
|
83
|
+
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
|
|
84
|
+
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The shell-local bin directory that fnm creates for one interactive shell. */
|
|
88
|
+
export function isFnmMultishellPath(path: string, posixPaths: boolean): boolean {
|
|
89
|
+
const normalized = (posixPaths
|
|
90
|
+
? posix.normalize(path)
|
|
91
|
+
: win32.normalize(path).replace(/\\/g, "/")).toLowerCase();
|
|
92
|
+
return /(?:^|\/)fnm_multishells?(?:\/|$)/.test(normalized);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Find the command the shell would actually use, including an OpenCodex shim. */
|
|
96
|
+
export function findFirstCodexOnPath(deps: CodexPathScanDeps = {}): CodexPathCandidate | null {
|
|
97
|
+
const exists = deps.exists ?? existsSync;
|
|
98
|
+
const shimFile = deps.isShimFile ?? inspectShimFile;
|
|
99
|
+
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
100
|
+
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
101
|
+
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
102
|
+
const joinPath = usePosix ? posix.join : win32.join;
|
|
103
|
+
const pathSep = usePosix ? ":" : win32.delimiter;
|
|
104
|
+
const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
|
|
105
|
+
const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
|
|
106
|
+
|
|
107
|
+
for (const dir of (deps.pathValue ?? process.env.PATH ?? "").split(pathSep).filter(Boolean)) {
|
|
108
|
+
if (wsl && isWindowsInteropDir(dir, automountRoot)) continue;
|
|
109
|
+
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
const path = joinPath(dir, name);
|
|
112
|
+
if (!exists(path) || isDir(path)) continue;
|
|
113
|
+
const isShim = shimFile(path);
|
|
114
|
+
if (isShim !== null) return { path, isShim };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a Codex command discovered through fnm's temporary PATH entry.
|
|
122
|
+
*
|
|
123
|
+
* A temporary path is safe to use only when its physical target is outside the
|
|
124
|
+
* temporary tree. Returning null is deliberate: callers must not wrap the
|
|
125
|
+
* shell-local path or silently choose a lower-priority Codex installation.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveStableFnmCodexPath(
|
|
128
|
+
path: string,
|
|
129
|
+
posixPaths: boolean,
|
|
130
|
+
realpath: ((path: string) => string) | undefined = realpathSync.native,
|
|
131
|
+
): string | null {
|
|
132
|
+
if (!isFnmMultishellPath(path, posixPaths)) return path;
|
|
133
|
+
try {
|
|
134
|
+
// fnm normally links the temporary directory to the durable installation,
|
|
135
|
+
// while npm may link the command file again into its package internals.
|
|
136
|
+
// Resolve only the directory so the command basename remains the stable
|
|
137
|
+
// installation entry that the installer is allowed to replace.
|
|
138
|
+
const pathTools = posixPaths ? posix : win32;
|
|
139
|
+
const resolvedParent = realpath(pathTools.dirname(path));
|
|
140
|
+
if (!resolvedParent || isFnmMultishellPath(resolvedParent, posixPaths) || !pathTools.isAbsolute(resolvedParent)) return null;
|
|
141
|
+
return pathTools.join(resolvedParent, pathTools.basename(path));
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/codex/shim-probe.ts
CHANGED
|
@@ -246,7 +246,7 @@ function readProbeMetadata(path: string, maxBytes: number): string | null {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
249
|
+
function probeUnixShimInstall(wrapperPath: string, childEnv?: NodeJS.ProcessEnv): UnixShimProbeResult {
|
|
250
250
|
if (process.platform === "win32") return null;
|
|
251
251
|
const probeDir = mkdtempSync(join(tmpdir(), "opencodex-shim-probe-"));
|
|
252
252
|
const markerPath = join(probeDir, "result");
|
|
@@ -255,6 +255,7 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
255
255
|
const stderrPath = join(probeDir, "stderr");
|
|
256
256
|
const env: NodeJS.ProcessEnv = {
|
|
257
257
|
...process.env,
|
|
258
|
+
...childEnv,
|
|
258
259
|
BUN_BE_BUN: "1",
|
|
259
260
|
OCX_SHIM_BYPASS: "1",
|
|
260
261
|
OCX_SHIM_PROBE: "1",
|
|
@@ -329,12 +330,12 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
|
|
332
|
-
function probeUnixShimFiles(files: readonly ShimFileState[]): UnixShimProbeResult {
|
|
333
|
+
function probeUnixShimFiles(files: readonly ShimFileState[], childEnv?: NodeJS.ProcessEnv): UnixShimProbeResult {
|
|
333
334
|
if (process.platform === "win32") return null;
|
|
334
335
|
codexShimProbeHookForTests?.();
|
|
335
336
|
return files
|
|
336
337
|
.filter(file => !file.preserveOnly)
|
|
337
|
-
.map(file => probeUnixShimInstall(file.wrapperPath))
|
|
338
|
+
.map(file => probeUnixShimInstall(file.wrapperPath, childEnv))
|
|
338
339
|
.find(result => result !== null) ?? null;
|
|
339
340
|
}
|
|
340
341
|
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
closeSync,
|
|
3
|
+
constants,
|
|
3
4
|
existsSync,
|
|
4
5
|
fstatSync,
|
|
5
6
|
lstatSync,
|
|
6
7
|
mkdirSync,
|
|
7
8
|
openSync,
|
|
8
9
|
readSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
unlinkSync,
|
|
9
12
|
writeFileSync,
|
|
10
13
|
type Stats,
|
|
11
14
|
} from "node:fs";
|
|
12
|
-
import {
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
17
|
import { getConfigDir } from "../config";
|
|
14
18
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
15
19
|
|
|
16
20
|
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
17
21
|
|
|
18
22
|
interface ShimState {
|
|
23
|
+
schema?: 2;
|
|
24
|
+
mode?: "path-overlay";
|
|
25
|
+
launcherPath?: string;
|
|
26
|
+
wrapperIdentity?: OverlayIdentity;
|
|
27
|
+
envIdentity?: OverlayIdentity;
|
|
28
|
+
transactionId?: string;
|
|
19
29
|
platform: NodeJS.Platform;
|
|
20
30
|
wrapperPath: string;
|
|
21
31
|
originalPath: string;
|
|
@@ -23,6 +33,51 @@ interface ShimState {
|
|
|
23
33
|
wrappers?: ShimFileState[];
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
export interface OverlayIdentity { dev: number; ino: number }
|
|
37
|
+
export interface OverlayState {
|
|
38
|
+
schema: 2;
|
|
39
|
+
mode: "path-overlay";
|
|
40
|
+
platform: NodeJS.Platform;
|
|
41
|
+
wrapperPath: string;
|
|
42
|
+
launcherPath: string;
|
|
43
|
+
// Older schema-2 records remain readable, but absent identities confer no mutation authority.
|
|
44
|
+
wrapperIdentity?: OverlayIdentity;
|
|
45
|
+
envIdentity?: OverlayIdentity;
|
|
46
|
+
transactionId?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function decodeOverlayState(value: unknown, configDir: string): OverlayState | null {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
51
|
+
const state = value as Record<string, unknown>;
|
|
52
|
+
const platforms = ["aix", "darwin", "freebsd", "linux", "openbsd", "sunos", "android", "netbsd", "haiku", "cygwin"];
|
|
53
|
+
const validPath = (path: unknown): path is string => typeof path === "string" && isAbsolute(path) && !/[\x00-\x1f\x7f]/.test(path);
|
|
54
|
+
if (state.schema !== 2 || state.mode !== "path-overlay" || !platforms.includes(String(state.platform))
|
|
55
|
+
|| !validPath(configDir) || !validPath(state.wrapperPath) || !validPath(state.launcherPath)
|
|
56
|
+
|| state.wrapperPath !== join(resolve(configDir), "bin", "codex")
|
|
57
|
+
|| dirname(state.wrapperPath).includes(":") || resolve(state.launcherPath) === state.wrapperPath) return null;
|
|
58
|
+
for (const field of ["originalPath", "backupPath", "realPath"]) {
|
|
59
|
+
if (state[field] !== undefined && state[field] !== state.launcherPath) return null;
|
|
60
|
+
}
|
|
61
|
+
if (state.preserveOnly !== undefined && state.preserveOnly !== false) return null;
|
|
62
|
+
if (state.wrappers !== undefined) {
|
|
63
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length !== 1) return null;
|
|
64
|
+
const file = state.wrappers[0];
|
|
65
|
+
if (!file || typeof file !== "object" || file.wrapperPath !== state.wrapperPath
|
|
66
|
+
|| file.originalPath !== state.launcherPath || file.backupPath !== state.launcherPath
|
|
67
|
+
|| (file.realPath !== undefined && file.realPath !== state.launcherPath) || file.preserveOnly) return null;
|
|
68
|
+
}
|
|
69
|
+
for (const key of ["wrapperIdentity", "envIdentity"]) {
|
|
70
|
+
const id = state[key] as Partial<OverlayIdentity> | undefined;
|
|
71
|
+
if (id !== undefined && (!id || !Number.isSafeInteger(id.dev) || !Number.isSafeInteger(id.ino))) return null;
|
|
72
|
+
}
|
|
73
|
+
if (state.transactionId !== undefined && (typeof state.transactionId !== "string" || !/^[a-f0-9-]{36}$/.test(state.transactionId))) return null;
|
|
74
|
+
return state as unknown as OverlayState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readOverlayState(configDir = getConfigDir()): OverlayState | null {
|
|
78
|
+
return decodeOverlayState(readStateResult(join(configDir, "codex-shim.json")).state, configDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
interface ShimFileState {
|
|
27
82
|
wrapperPath: string;
|
|
28
83
|
originalPath: string;
|
|
@@ -43,7 +98,7 @@ function fileErrorCode(error: unknown): string | undefined {
|
|
|
43
98
|
: undefined;
|
|
44
99
|
}
|
|
45
100
|
|
|
46
|
-
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
|
|
101
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string; stat: Stats } | { warning: string } | null {
|
|
47
102
|
let lexicalBefore: Stats;
|
|
48
103
|
try {
|
|
49
104
|
lexicalBefore = lstatSync(path);
|
|
@@ -56,7 +111,7 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
|
|
|
56
111
|
}
|
|
57
112
|
let fd: number;
|
|
58
113
|
try {
|
|
59
|
-
fd = openSync(path, "r");
|
|
114
|
+
fd = openSync(path, process.platform === "win32" ? "r" : constants.O_RDONLY | constants.O_NONBLOCK);
|
|
60
115
|
} catch (error) {
|
|
61
116
|
if (fileErrorCode(error) === "ENOENT") return null;
|
|
62
117
|
return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
|
|
@@ -87,11 +142,16 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
|
|
|
87
142
|
}
|
|
88
143
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
89
144
|
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
145
|
+
|| before.mode !== after.mode || before.uid !== after.uid
|
|
90
146
|
|| lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
|
|
91
|
-
||
|
|
147
|
+
|| lexicalBefore.mode !== before.mode || lexicalBefore.uid !== before.uid
|
|
148
|
+
|| lexicalBefore.size !== before.size || lexicalBefore.mtimeMs !== before.mtimeMs || lexicalBefore.ctimeMs !== before.ctimeMs
|
|
149
|
+
|| !lexicalAfter.isFile() || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino
|
|
150
|
+
|| lexicalAfter.mode !== after.mode || lexicalAfter.uid !== after.uid
|
|
151
|
+
|| lexicalAfter.size !== after.size || lexicalAfter.mtimeMs !== after.mtimeMs || lexicalAfter.ctimeMs !== after.ctimeMs) {
|
|
92
152
|
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
93
153
|
}
|
|
94
|
-
return { bytes: buffer, content: buffer.toString("utf8") };
|
|
154
|
+
return { bytes: buffer, content: buffer.toString("utf8"), stat: after };
|
|
95
155
|
} finally {
|
|
96
156
|
closeSync(fd);
|
|
97
157
|
}
|
|
@@ -105,6 +165,15 @@ function readStateResult(path = statePath()): ShimStateReadResult {
|
|
|
105
165
|
const value = JSON.parse(bounded.content) as unknown;
|
|
106
166
|
if (!value || typeof value !== "object") return { state: null, present: true };
|
|
107
167
|
const state = value as Record<string, unknown>;
|
|
168
|
+
if (state.mode !== undefined || state.schema !== undefined) {
|
|
169
|
+
// Legacy records stay readable; private overlay authority requires safe ownership.
|
|
170
|
+
// Non-writable mode drift is readable for opt-in repair, never a healthy installation.
|
|
171
|
+
if (process.platform !== "win32" && (bounded.stat.uid !== process.getuid?.() || (bounded.stat.mode & 0o022))) {
|
|
172
|
+
return { state: null, present: true, warning: "Private Codex shim state is not owned by the current user or is group/world writable; preserving artifacts. Remove the private files manually before reinstalling." };
|
|
173
|
+
}
|
|
174
|
+
const overlay = decodeOverlayState(value, dirname(path));
|
|
175
|
+
return { state: overlay ? { ...overlay, originalPath: overlay.launcherPath, backupPath: overlay.launcherPath } : null, present: true };
|
|
176
|
+
}
|
|
108
177
|
if (typeof state.platform !== "string") return { state: null, present: true };
|
|
109
178
|
const validFile = (item: unknown): item is ShimFileState => {
|
|
110
179
|
if (!item || typeof item !== "object") return false;
|
|
@@ -134,18 +203,35 @@ function statePath(): string {
|
|
|
134
203
|
return join(getConfigDir(), "codex-shim.json");
|
|
135
204
|
}
|
|
136
205
|
|
|
137
|
-
function writeState(state: ShimState): void {
|
|
206
|
+
function writeState(state: ShimState | OverlayState): void {
|
|
138
207
|
const path = statePath();
|
|
139
208
|
recordOwnedConfigPath(getConfigDir(), path);
|
|
140
209
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
210
|
+
if (state.mode === "path-overlay") {
|
|
211
|
+
if (!decodeOverlayState(state, getConfigDir()) || !state.wrapperIdentity || !state.envIdentity) {
|
|
212
|
+
throw new Error("Invalid Codex overlay state: recorded ownership identities are required");
|
|
213
|
+
}
|
|
214
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
215
|
+
try {
|
|
216
|
+
writeFileSync(staged, JSON.stringify(state, null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
217
|
+
renameSync(staged, path);
|
|
218
|
+
} finally {
|
|
219
|
+
try { unlinkSync(staged); } catch { /* committed or absent */ }
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
141
223
|
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
142
224
|
}
|
|
143
225
|
|
|
144
226
|
function stateFiles(state: ShimState): ShimFileState[] {
|
|
227
|
+
if (state.mode === "path-overlay" && state.launcherPath) return [{
|
|
228
|
+
wrapperPath: state.wrapperPath, originalPath: state.launcherPath,
|
|
229
|
+
backupPath: state.launcherPath, realPath: state.launcherPath,
|
|
230
|
+
}];
|
|
145
231
|
return state.wrappers?.length
|
|
146
232
|
? state.wrappers
|
|
147
233
|
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
148
234
|
}
|
|
149
235
|
|
|
150
236
|
export type { ShimState, ShimFileState };
|
|
151
|
-
export { fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
|
|
237
|
+
export { readBoundedRegularFile, fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
|
package/src/codex/shim.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
unlinkSync,
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
|
-
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
11
|
+
import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
|
|
12
12
|
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
13
13
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
14
14
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
@@ -53,14 +53,28 @@ import {
|
|
|
53
53
|
type UnixShimProbeResult,
|
|
54
54
|
} from "./shim-probe";
|
|
55
55
|
import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
|
|
56
|
+
import {
|
|
57
|
+
isWindowsInteropDir,
|
|
58
|
+
isExecutableCodexCandidate,
|
|
59
|
+
inspectShimFile,
|
|
60
|
+
realIsDirectory,
|
|
61
|
+
resolveStableFnmCodexPath,
|
|
62
|
+
type CodexPathScanDeps,
|
|
63
|
+
} from "./shim-path-resolution";
|
|
56
64
|
|
|
57
65
|
export { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim } from "./shim-templates";
|
|
58
66
|
export { isVersionManagerOwnedCodexPath } from "./shim-fingerprint";
|
|
59
67
|
export { CODEX_SHIM_STATE_MAX_BYTES } from "./shim-state-file";
|
|
60
68
|
export { setCodexShimProbeHookForTests, setCodexShimProbeShellForTests, setCodexShimProbeObservationMsForTests } from "./shim-probe";
|
|
61
69
|
export type { CodexShimBackingForCommand } from "./shim-inspect";
|
|
70
|
+
export type { CodexPathScanDeps } from "./shim-path-resolution";
|
|
71
|
+
export { isWindowsInteropDir } from "./shim-path-resolution";
|
|
62
72
|
export { isLocalAbsoluteInspectionPath, inspectCodexShimBackingForCommand } from "./shim-inspect";
|
|
63
73
|
|
|
74
|
+
import { isShim, diagnoseCodexShim } from "./shim-diagnostics";
|
|
75
|
+
import { installUnixOverlay, autoRestoreUnixOverlay, uninstallUnixCodexShim, overlayPaths } from "./shim-overlay";
|
|
76
|
+
export { diagnoseCodexShim, codexShimStatus, type CodexShimDiagnostic } from "./shim-diagnostics";
|
|
77
|
+
|
|
64
78
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
65
79
|
|
|
66
80
|
let lastShimDiscoveryError: string | null = null;
|
|
@@ -75,6 +89,17 @@ interface InstallCodexShimInternalOptions {
|
|
|
75
89
|
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
76
90
|
}
|
|
77
91
|
|
|
92
|
+
function discoveredCodexPath(path: string, posixPaths: boolean, realpath?: (path: string) => string): string | null {
|
|
93
|
+
const resolved = resolveStableFnmCodexPath(path, posixPaths, realpath);
|
|
94
|
+
if (resolved !== null) return resolved;
|
|
95
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
96
|
+
`Found codex at ${path} inside fnm's temporary multishell path, but it did not resolve to a durable Node installation. `
|
|
97
|
+
+ "Refusing to install a shim; retry after fnm selects a stable installation.",
|
|
98
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
99
|
+
);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
export type CodexShimAutoRestoreResult =
|
|
79
104
|
| { status: "not-installed" | "healthy" | "disabled" }
|
|
80
105
|
| { status: "ineligible" | "deferred"; message?: string }
|
|
@@ -95,65 +120,18 @@ function commandNames(name: string): string[] {
|
|
|
95
120
|
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
function isShim(path: string): boolean {
|
|
99
|
-
try {
|
|
100
|
-
return readFileSync(path, "utf8").includes(SHIM_MARKER);
|
|
101
|
-
} catch {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
107
|
-
try {
|
|
108
|
-
const content = readFileSync(path, "utf8");
|
|
109
|
-
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
110
|
-
if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
|
|
111
|
-
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
112
|
-
return true;
|
|
113
|
-
} catch {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* A PATH entry that reaches Windows through WSL drive interop
|
|
120
|
-
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
121
|
-
* /etc/wsl.conf [automount] root).
|
|
122
|
-
*/
|
|
123
|
-
export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
|
|
124
|
-
const root = automountRoot.replace(/\/+$/, "");
|
|
125
|
-
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
-
return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type CodexPathScanDeps = {
|
|
130
|
-
pathValue?: string;
|
|
131
|
-
wsl?: boolean;
|
|
132
|
-
/** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
|
|
133
|
-
posixPaths?: boolean;
|
|
134
|
-
automountRoot?: string;
|
|
135
|
-
exists?: (path: string) => boolean;
|
|
136
|
-
isShimFile?: (path: string) => boolean;
|
|
137
|
-
isDirectory?: (path: string) => boolean;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
function realIsDirectory(path: string): boolean {
|
|
141
|
-
try {
|
|
142
|
-
return lstatSync(path).isDirectory();
|
|
143
|
-
} catch {
|
|
144
|
-
return true; // unreadable -> treat as unusable
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
123
|
export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
149
124
|
lastShimDiscoveryError = null;
|
|
150
125
|
const exists = deps.exists ?? existsSync;
|
|
151
|
-
const shimFile = deps.isShimFile ?? isShim;
|
|
126
|
+
const shimFile = deps.isShimFile ?? (process.platform === "win32" ? isShim : (path: string) => inspectShimFile(path) !== false);
|
|
152
127
|
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
128
|
+
const eligible = deps.executableEligible ?? (deps.exists ? () => true : isExecutableCodexCandidate);
|
|
153
129
|
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
154
130
|
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
155
|
-
|
|
156
|
-
|
|
131
|
+
// The injected path flavour governs join and separator alike; host-native `join` and
|
|
132
|
+
// `delimiter` made a Windows-flavoured scan split `C:\...` on a Linux host.
|
|
133
|
+
const joinPath = usePosix ? posix.join : win32.join;
|
|
134
|
+
const pathSep = usePosix ? ":" : win32.delimiter;
|
|
157
135
|
const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
|
|
158
136
|
// Windows npm prefixes ship codex.exe/codex.cmd next to the extensionless sh launcher.
|
|
159
137
|
const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
|
|
@@ -175,8 +153,14 @@ export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
|
175
153
|
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
176
154
|
for (const name of names) {
|
|
177
155
|
const path = joinPath(dir, name);
|
|
178
|
-
if (!exists(path)
|
|
179
|
-
if (!
|
|
156
|
+
if (!exists(path)) continue;
|
|
157
|
+
if (usePosix && (path === overlayPaths().wrapper || !eligible(path))) continue;
|
|
158
|
+
if (shimFile(path)) continue;
|
|
159
|
+
if (!isDir(path)) {
|
|
160
|
+
const resolved = discoveredCodexPath(path, usePosix, deps.realpath);
|
|
161
|
+
if (resolved !== null) return resolved;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
180
164
|
}
|
|
181
165
|
}
|
|
182
166
|
|
|
@@ -195,11 +179,13 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
|
195
179
|
lastShimDiscoveryError = null;
|
|
196
180
|
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
197
181
|
const exe = join(dir, "codex.exe");
|
|
198
|
-
|
|
182
|
+
const stableExe = existsSync(exe) && !isShim(exe) ? discoveredCodexPath(exe, false) : exe;
|
|
183
|
+
if (stableExe === null) return null;
|
|
184
|
+
if (existsSync(stableExe) && !isShim(stableExe)) {
|
|
199
185
|
try {
|
|
200
|
-
if (!lstatSync(
|
|
186
|
+
if (!lstatSync(stableExe).isDirectory()) {
|
|
201
187
|
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
202
|
-
`Found codex.exe at ${
|
|
188
|
+
`Found codex.exe at ${stableExe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
|
|
203
189
|
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.",
|
|
204
190
|
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
205
191
|
);
|
|
@@ -216,9 +202,12 @@ function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
|
216
202
|
const targets: ShimFileState[] = [];
|
|
217
203
|
for (const path of [cmd, ps1, gitBashLauncher]) {
|
|
218
204
|
if (!existsSync(path) || isShim(path)) continue;
|
|
205
|
+
const stablePath = discoveredCodexPath(path, false);
|
|
206
|
+
if (stablePath === null) return null;
|
|
207
|
+
if (!existsSync(stablePath) || isShim(stablePath)) continue;
|
|
219
208
|
try {
|
|
220
|
-
if (!lstatSync(
|
|
221
|
-
targets.push({ wrapperPath:
|
|
209
|
+
if (!lstatSync(stablePath).isDirectory()) {
|
|
210
|
+
targets.push({ wrapperPath: stablePath, originalPath: stablePath, backupPath: backupPathFor(stablePath) });
|
|
222
211
|
}
|
|
223
212
|
} catch { /* keep scanning */ }
|
|
224
213
|
}
|
|
@@ -1072,7 +1061,8 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1072
1061
|
};
|
|
1073
1062
|
}
|
|
1074
1063
|
|
|
1075
|
-
export function installCodexShim(): { installed: boolean; message: string } {
|
|
1064
|
+
export function installCodexShim(): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
|
|
1065
|
+
if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
|
|
1076
1066
|
return installCodexShimInternal({ allowFreshInstall: true });
|
|
1077
1067
|
}
|
|
1078
1068
|
|
|
@@ -1086,6 +1076,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1086
1076
|
/** Narrow deterministic race seam for the guarded transaction tests. */
|
|
1087
1077
|
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
1088
1078
|
}): CodexShimAutoRestoreResult {
|
|
1079
|
+
if (process.platform !== "win32") return autoRestoreUnixOverlay(options);
|
|
1089
1080
|
const stateRead = readStateResult();
|
|
1090
1081
|
const state = stateRead.state;
|
|
1091
1082
|
if (!state) {
|
|
@@ -1181,6 +1172,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1181
1172
|
}
|
|
1182
1173
|
|
|
1183
1174
|
export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
1175
|
+
if (process.platform !== "win32") return uninstallUnixCodexShim();
|
|
1184
1176
|
const state = readState();
|
|
1185
1177
|
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
1186
1178
|
const files = stateFiles(state);
|
|
@@ -1199,48 +1191,3 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
|
1199
1191
|
export function isCodexShimInstalled(): boolean {
|
|
1200
1192
|
return diagnoseCodexShim().installed;
|
|
1201
1193
|
}
|
|
1202
|
-
|
|
1203
|
-
export interface CodexShimDiagnostic {
|
|
1204
|
-
installed: boolean;
|
|
1205
|
-
healthy: boolean;
|
|
1206
|
-
summary: string;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
1210
|
-
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
1211
|
-
const state = readState();
|
|
1212
|
-
if (!state) {
|
|
1213
|
-
if (existsSync(statePath())) {
|
|
1214
|
-
return {
|
|
1215
|
-
installed: true,
|
|
1216
|
-
healthy: false,
|
|
1217
|
-
summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
return {
|
|
1221
|
-
installed: false,
|
|
1222
|
-
healthy: false,
|
|
1223
|
-
summary: "Codex autostart shim is not installed.",
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
const files = stateFiles(state);
|
|
1227
|
-
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
1228
|
-
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
1229
|
-
: existsSync(file.wrapperPath)
|
|
1230
|
-
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
1231
|
-
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
1232
|
-
const summary = files.map(file => {
|
|
1233
|
-
const wrapper = existsSync(file.wrapperPath)
|
|
1234
|
-
? isShim(file.wrapperPath)
|
|
1235
|
-
? "shim present"
|
|
1236
|
-
: "present but not an opencodex shim"
|
|
1237
|
-
: "missing";
|
|
1238
|
-
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
1239
|
-
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
1240
|
-
}).join("\n");
|
|
1241
|
-
return { installed: true, healthy, summary };
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
export function codexShimStatus(): string {
|
|
1245
|
-
return diagnoseCodexShim().summary;
|
|
1246
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { codexAccountUsesCreditsAfterLimit } from "./account-credit-use";
|
|
1
2
|
/**
|
|
2
3
|
* Quota-aware subagent model fallback (issue #374).
|
|
3
4
|
*
|
|
@@ -233,7 +234,7 @@ export function isNativeModelQuotaExhausted(
|
|
|
233
234
|
// Subagent fallback reads the same score, so a stale terminal reading would push
|
|
234
235
|
// subagents off a native model whose window has already reset. Thread the caller's clock
|
|
235
236
|
// rather than letting the scorer read wall time - the two would silently diverge.
|
|
236
|
-
const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now);
|
|
237
|
+
const usage = computeCodexUsageScore(quota, getPoolAccountPlan(config, resolvedAccountId), now, codexAccountUsesCreditsAfterLimit(config, resolvedAccountId));
|
|
237
238
|
if (usage >= CODEX_UNKNOWN_USAGE_SCORE) return false;
|
|
238
239
|
return usage >= quotaThreshold(config, resolvedAccountId);
|
|
239
240
|
}
|