@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isEffectiveCodexDesktopAuthless } from "../loopback-target";
|
|
3
|
+
import { NATIVE_RESERVE_MODEL } from "./native-models";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #4811: why an opted-in install still gets no Luna Reserve row.
|
|
7
|
+
*
|
|
8
|
+
* The Reserve projection is account-qualified — its slug is `<selector>/gpt-reserve` — so it
|
|
9
|
+
* cannot be written without a selector that targets the main Codex account. On a fresh authless
|
|
10
|
+
* install `codexAccountNamespaces` is empty, the account picker is therefore off, no selectors
|
|
11
|
+
* exist, and `createReserveCatalogProjection` returns `undefined`. Nothing downstream can
|
|
12
|
+
* explain that: an omitted row has no place to carry a reason, which is exactly the shape of
|
|
13
|
+
* #4212 for account-gated natives, and the same answer applies — say it once, here, while the
|
|
14
|
+
* inputs that produced the omission are still in scope.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately scoped to an operator who asked for the feature. A default install has
|
|
17
|
+
* `codexDesktopAuthless` unset, wants no Reserve row, and must not be told about one. An install
|
|
18
|
+
* that stores the flag on a bind where it cannot take effect is a different silence, already
|
|
19
|
+
* reported as `inertReason` by `describeCodexDesktopSwitches`, and is not repeated here.
|
|
20
|
+
*/
|
|
21
|
+
export type ReserveCatalogSuppressionReason =
|
|
22
|
+
| "no-canonical-openai-provider"
|
|
23
|
+
| "account-picker-disabled"
|
|
24
|
+
| "no-account-namespaces"
|
|
25
|
+
| "no-main-account-selector";
|
|
26
|
+
|
|
27
|
+
type ReserveSuppressionConfig = Pick<
|
|
28
|
+
OcxConfig,
|
|
29
|
+
| "runtimeRole"
|
|
30
|
+
| "hostname"
|
|
31
|
+
| "unauthenticatedLoopbackListener"
|
|
32
|
+
| "codexDesktopAuthless"
|
|
33
|
+
| "codexAccountNamespaces"
|
|
34
|
+
| "codexAccountPickerEnabled"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export interface ReserveSuppressionInput {
|
|
38
|
+
/** Already resolved by the caller; passed in so this module stays off the metadata graph. */
|
|
39
|
+
includeAccountBoundNativeOpenAi: boolean;
|
|
40
|
+
/** Selectors that resolved to the main Codex account. A non-empty list means no suppression. */
|
|
41
|
+
mainSelectors: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reserveCatalogSuppressionReason(
|
|
45
|
+
config: ReserveSuppressionConfig,
|
|
46
|
+
input: ReserveSuppressionInput,
|
|
47
|
+
): ReserveCatalogSuppressionReason | undefined {
|
|
48
|
+
if (!isEffectiveCodexDesktopAuthless(config) || input.mainSelectors.length > 0) return undefined;
|
|
49
|
+
if (!input.includeAccountBoundNativeOpenAi) return "no-canonical-openai-provider";
|
|
50
|
+
// Checked before the empty-map case: when an operator has explicitly turned the picker off,
|
|
51
|
+
// naming the empty map would send them to hand-author selectors that the opt-in generates.
|
|
52
|
+
if (config.codexAccountPickerEnabled === false) return "account-picker-disabled";
|
|
53
|
+
if (Object.keys(config.codexAccountNamespaces ?? {}).length === 0) return "no-account-namespaces";
|
|
54
|
+
return "no-main-account-selector";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SUPPRESSION_TEXT: Readonly<Record<ReserveCatalogSuppressionReason, {
|
|
58
|
+
cause: string;
|
|
59
|
+
action: string;
|
|
60
|
+
}>> = {
|
|
61
|
+
"no-canonical-openai-provider": {
|
|
62
|
+
cause: "no enabled canonical OpenAI provider can serve account-qualified native routes",
|
|
63
|
+
action: "Enable the built-in \"openai\" provider with forwarded ChatGPT auth.",
|
|
64
|
+
},
|
|
65
|
+
"account-picker-disabled": {
|
|
66
|
+
cause: "the Codex account picker is turned off (codexAccountPickerEnabled is false)",
|
|
67
|
+
action: "Set codexAccountPickerEnabled to true to generate the default account selectors.",
|
|
68
|
+
},
|
|
69
|
+
"no-account-namespaces": {
|
|
70
|
+
cause: "no Codex account selectors are configured (codexAccountNamespaces is empty)",
|
|
71
|
+
action: "Enable the Codex account picker once to generate the default account selectors.",
|
|
72
|
+
},
|
|
73
|
+
"no-main-account-selector": {
|
|
74
|
+
cause: "no configured selector in codexAccountNamespaces targets the main Codex account",
|
|
75
|
+
action: "Map one selector to the main Codex account.",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const warnedReserveSuppression = new Set<string>();
|
|
80
|
+
|
|
81
|
+
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
82
|
+
export function resetReserveSuppressionWarningsForTests(): void {
|
|
83
|
+
warnedReserveSuppression.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function warnReserveSuppressedOnce(reason: ReserveCatalogSuppressionReason): void {
|
|
87
|
+
if (warnedReserveSuppression.has(reason)) return;
|
|
88
|
+
warnedReserveSuppression.add(reason);
|
|
89
|
+
const text = SUPPRESSION_TEXT[reason];
|
|
90
|
+
console.warn(
|
|
91
|
+
`[opencodex] catalog sync: authless Codex Desktop routing is on, but the Luna Reserve row `
|
|
92
|
+
+ `(<selector>/${NATIVE_RESERVE_MODEL}) is not being written because ${text.cause}. `
|
|
93
|
+
+ `The Reserve projection is account-qualified and needs a selector for the main Codex `
|
|
94
|
+
+ `account. ${text.action}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
readCodexCatalogPath,
|
|
34
34
|
readCodexCatalogPathForHome,
|
|
35
35
|
readNativeBaseline,
|
|
36
|
+
nativeMultiAgentDefaults,
|
|
36
37
|
} from "./parsing";
|
|
37
38
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
38
39
|
import {
|
|
@@ -50,6 +51,7 @@ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
|
50
51
|
import { bundledCatalogCacheState, loadBundledCodexCatalog } from "./bundled";
|
|
51
52
|
import { isMultiAgentV2Enabled } from "../features";
|
|
52
53
|
import { clampCatalogModelsToCodexSupport } from "./effort";
|
|
54
|
+
import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
|
|
53
55
|
import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
|
|
54
56
|
import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
55
57
|
import {
|
|
@@ -57,10 +59,12 @@ import {
|
|
|
57
59
|
type CatalogWritePermit,
|
|
58
60
|
} from "../catalog-write-serialization";
|
|
59
61
|
import {
|
|
62
|
+
preparedBytesDifferFromDisk,
|
|
60
63
|
publishHashedCodexCatalogBackup,
|
|
61
64
|
publishLegacyCodexCatalogBackup,
|
|
62
65
|
replaceActiveCodexCatalog,
|
|
63
66
|
replaceCodexModelsCache,
|
|
67
|
+
type PreparedCatalogFileWrite,
|
|
64
68
|
} from "../internal/catalog-writer";
|
|
65
69
|
import { visibleCodexAccountSelectors } from "./account-models";
|
|
66
70
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
@@ -75,6 +79,7 @@ import {
|
|
|
75
79
|
import { finishUpstreamNativeEntry } from "./derive-entry";
|
|
76
80
|
import { finalizeAutoReviewModelOverride } from "./auto-review";
|
|
77
81
|
import { gatedNativeAccountLabel, gatedNativeReauthSuppressionReason, warnGatedNativeSuppressedOnce } from "./gated-native-warn";
|
|
82
|
+
import { reserveCatalogSuppressionReason, warnReserveSuppressedOnce } from "./reserve-warn";
|
|
78
83
|
|
|
79
84
|
interface RetainedCatalogSyncRead {
|
|
80
85
|
readonly catalogPath: string;
|
|
@@ -231,24 +236,6 @@ function revalidateRetainedCatalogSync(
|
|
|
231
236
|
};
|
|
232
237
|
}
|
|
233
238
|
|
|
234
|
-
/**
|
|
235
|
-
* Exact bytes currently on disk at `path`, or null when unreadable/absent.
|
|
236
|
-
*
|
|
237
|
-
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
238
|
-
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
239
|
-
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
|
|
240
|
-
* decoded strings would then classify a malformed catalog as identical, skip the
|
|
241
|
-
* atomic repair write, and leave the corruption on disk while reporting
|
|
242
|
-
* `catalogWritten: false`.
|
|
243
|
-
*/
|
|
244
|
-
function currentCatalogFileContent(path: string): Buffer | null {
|
|
245
|
-
try {
|
|
246
|
-
return readFileSync(path);
|
|
247
|
-
} catch {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
239
|
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
253
240
|
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
254
241
|
try {
|
|
@@ -320,6 +307,11 @@ function writeRetainedCatalogSync({
|
|
|
320
307
|
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
321
308
|
const featured = config.subagentModels ?? [];
|
|
322
309
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
310
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(
|
|
311
|
+
config,
|
|
312
|
+
orderedGoModels,
|
|
313
|
+
catalogModelsForMerge,
|
|
314
|
+
);
|
|
323
315
|
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
324
316
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
325
317
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
@@ -376,6 +368,14 @@ function writeRetainedCatalogSync({
|
|
|
376
368
|
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
377
369
|
const reserveMainSelectors = accountSelectors.filter(selector =>
|
|
378
370
|
isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
|
|
371
|
+
// #4811: an omitted Reserve row carries no reason, so the explanation has to be emitted here,
|
|
372
|
+
// where the selector inputs that produced the omission are still in scope. Silent for every
|
|
373
|
+
// install that did not opt into authless Codex Desktop routing.
|
|
374
|
+
const reserveSuppression = reserveCatalogSuppressionReason(config, {
|
|
375
|
+
includeAccountBoundNativeOpenAi,
|
|
376
|
+
mainSelectors: reserveMainSelectors,
|
|
377
|
+
});
|
|
378
|
+
if (reserveSuppression) warnReserveSuppressedOnce(reserveSuppression);
|
|
379
379
|
// The active file can own a bare source even when the bundled catalog is the build base.
|
|
380
380
|
// A previously clamped qualified projection must not shorten a retained genuine ladder.
|
|
381
381
|
const reserveObservations = [
|
|
@@ -443,6 +443,7 @@ function writeRetainedCatalogSync({
|
|
|
443
443
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
444
444
|
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
445
445
|
const baseline = readNativeBaseline(catalogPath);
|
|
446
|
+
const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models);
|
|
446
447
|
const gatheredProviderNames = new Set(
|
|
447
448
|
Object.entries(config.providers ?? {})
|
|
448
449
|
.filter(([, prov]) => prov.disabled !== true)
|
|
@@ -512,8 +513,10 @@ function writeRetainedCatalogSync({
|
|
|
512
513
|
includeNativeOpenAi,
|
|
513
514
|
accountBoundEntries,
|
|
514
515
|
suppressedBareNativeSlugs,
|
|
516
|
+
suppressedSyntheticMaxSlugs,
|
|
515
517
|
openaiContextCap,
|
|
516
518
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
519
|
+
nativeMultiAgentDefaults: nativePinBaseline,
|
|
517
520
|
policy: {
|
|
518
521
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
519
522
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -539,15 +542,12 @@ function writeRetainedCatalogSync({
|
|
|
539
542
|
// nothing about the catalog changed. Skipping the no-op write keeps both the mtime
|
|
540
543
|
// and `catalogWritten` honest; `added` still reports the routed rows the catalog
|
|
541
544
|
// carries, because they are on disk either way.
|
|
542
|
-
const
|
|
543
|
-
if (
|
|
545
|
+
const preparedCatalog: PreparedCatalogFileWrite = { path: catalogPath, content };
|
|
546
|
+
if (!preparedBytesDifferFromDisk(preparedCatalog)) {
|
|
544
547
|
return { added, path: catalogPath, catalogWritten: false, comboOmissions };
|
|
545
548
|
}
|
|
546
549
|
|
|
547
|
-
replaceActiveCodexCatalog(permit, owningCodexHome,
|
|
548
|
-
path: catalogPath,
|
|
549
|
-
content,
|
|
550
|
-
});
|
|
550
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
|
|
551
551
|
return {
|
|
552
552
|
added,
|
|
553
553
|
path: catalogPath,
|
|
@@ -694,10 +694,25 @@ export function invalidateCodexModelsCacheWithPermit(
|
|
|
694
694
|
client_version: "0.0.0",
|
|
695
695
|
models: [...models, ...observedAccountModels],
|
|
696
696
|
};
|
|
697
|
-
|
|
697
|
+
const preparedCache: PreparedCatalogFileWrite = {
|
|
698
698
|
path: cachePath,
|
|
699
699
|
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
700
|
-
}
|
|
700
|
+
};
|
|
701
|
+
// The same no-op rule the active catalog already applies (#1459), for the same
|
|
702
|
+
// reason and at the second writer that has to obey it.
|
|
703
|
+
//
|
|
704
|
+
// This function is what `refreshCodexModelCatalog` reports as `cacheSynced`, and
|
|
705
|
+
// `handleStart` ORs that into the stale-app-server warning. Rewriting identical
|
|
706
|
+
// bytes bumped this file's mtime and returned `true`, so on a start where the
|
|
707
|
+
// catalog reproduced byte-identically — the settled case — the warning still
|
|
708
|
+
// claimed "Disk catalog/cache were updated" and told the operator their Codex
|
|
709
|
+
// model list might be stale, when nothing on disk had changed and Codex held the
|
|
710
|
+
// same model set the file already described. Returning `false` here makes
|
|
711
|
+
// `cacheSynced` mean what its name and its consumers already assume, and what
|
|
712
|
+
// `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
|
|
713
|
+
// already assert: a write happened.
|
|
714
|
+
if (!preparedBytesDifferFromDisk(preparedCache)) return false;
|
|
715
|
+
replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
|
|
701
716
|
return true;
|
|
702
717
|
} catch {
|
|
703
718
|
return false;
|
|
@@ -113,6 +113,15 @@ export interface GatherRoutedModelsOptions {
|
|
|
113
113
|
providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
|
|
114
114
|
/** Internal convergence sink for the immutable policy that produced the returned rows. */
|
|
115
115
|
discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
|
|
116
|
+
/**
|
|
117
|
+
* Each provider's cache content revision as of the moment its rows were chosen.
|
|
118
|
+
*
|
|
119
|
+
* A caller that derives something from these rows and wants to know later whether the rows are
|
|
120
|
+
* still current must use this, not a revision sampled after the gather returns: another flight
|
|
121
|
+
* can publish between the choice and the sample, and the derived value would then carry that
|
|
122
|
+
* flight's identity while holding these rows.
|
|
123
|
+
*/
|
|
124
|
+
providerContentRevisions?: Map<string, string>;
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
interface GatherFlightResult {
|
|
@@ -121,6 +130,7 @@ interface GatherFlightResult {
|
|
|
121
130
|
providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
122
131
|
providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
123
132
|
discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
133
|
+
providerContentRevisions: ReadonlyMap<string, string>;
|
|
124
134
|
}
|
|
125
135
|
interface GatherInflightEntry {
|
|
126
136
|
readonly discoveryPolicyIdentity: string;
|
|
@@ -256,6 +266,7 @@ async function gatherRoutedModelsWithAuth(
|
|
|
256
266
|
providerAuthOutcomes,
|
|
257
267
|
providerModelOutcomes,
|
|
258
268
|
discoveryPolicySnapshots,
|
|
269
|
+
providerContentRevisions,
|
|
259
270
|
} = await entry.promise;
|
|
260
271
|
if (options?.comboOmissions) {
|
|
261
272
|
options.comboOmissions.length = 0;
|
|
@@ -269,6 +280,12 @@ async function gatherRoutedModelsWithAuth(
|
|
|
269
280
|
options.providerModelOutcomes.length = 0;
|
|
270
281
|
options.providerModelOutcomes.push(...providerModelOutcomes);
|
|
271
282
|
}
|
|
283
|
+
if (options?.providerContentRevisions) {
|
|
284
|
+
options.providerContentRevisions.clear();
|
|
285
|
+
for (const [provider, revision] of providerContentRevisions) {
|
|
286
|
+
options.providerContentRevisions.set(provider, revision);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
272
289
|
if (options?.discoveryPolicySnapshots) {
|
|
273
290
|
options.discoveryPolicySnapshots.length = 0;
|
|
274
291
|
options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
|
|
@@ -763,6 +780,8 @@ async function gatherRoutedModelsUncached(
|
|
|
763
780
|
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
764
781
|
providerModelOutcomes,
|
|
765
782
|
discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
|
|
783
|
+
// Stamped by each provider at the moment it chose its rows, not sampled here.
|
|
784
|
+
providerContentRevisions: new Map(providerResults.map(result => [result.outcome.provider, result.contentRevision])),
|
|
766
785
|
};
|
|
767
786
|
}
|
|
768
787
|
|
|
@@ -850,6 +869,26 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
|
850
869
|
];
|
|
851
870
|
}
|
|
852
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Add generated-registry rows the live provider list did not return, and backfill a
|
|
874
|
+
* published context window onto a live row that arrived without one.
|
|
875
|
+
*
|
|
876
|
+
* The backfill is the reason this function has a merge branch at all. It used to skip every
|
|
877
|
+
* id the live list returned, which is correct for a row that already knows its window and
|
|
878
|
+
* wrong for the normal OpenCode Go case, where discovery returns an id with no context
|
|
879
|
+
* field at all. The serialized Codex catalog was unaffected, because
|
|
880
|
+
* `applyCatalogMetadata` writes `context_window` onto the entry straight from the generated
|
|
881
|
+
* table; the absence was only visible to consumers that read `CatalogModel.contextWindow`
|
|
882
|
+
* (#4971). Those are not cosmetic: `buildClaudeContextWindows` drops a routed row with no
|
|
883
|
+
* window from the map that decides the `[1m]` marker, so a published 1M model could not be
|
|
884
|
+
* recognized as one, and the Grok config writer and several client exporters omit the field
|
|
885
|
+
* entirely rather than emit a known value.
|
|
886
|
+
*
|
|
887
|
+
* It is a missing-value fill, never an override: a live positive window still wins, because
|
|
888
|
+
* the upstream is the authority on its own model. The seeded row is re-hinted so operator
|
|
889
|
+
* precedence is unchanged — a configured window still lowers it and `providerContextCaps`
|
|
890
|
+
* still caps it, exactly as for an appended row.
|
|
891
|
+
*/
|
|
853
892
|
export function augmentRoutedModelsWithMetadata(
|
|
854
893
|
models: CatalogModel[],
|
|
855
894
|
providerNames: string[],
|
|
@@ -858,7 +897,7 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
858
897
|
metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
|
|
859
898
|
): CatalogModel[] {
|
|
860
899
|
const out = [...models];
|
|
861
|
-
const
|
|
900
|
+
const indexByKey = new Map(out.map((model, index) => [`${model.provider}/${model.id}`, index]));
|
|
862
901
|
for (const provider of providerNames) {
|
|
863
902
|
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
864
903
|
if (providers?.[provider]?.liveModels === false) continue;
|
|
@@ -866,9 +905,28 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
866
905
|
if (!jawcodeProvider) continue;
|
|
867
906
|
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
868
907
|
const key = `${provider}/${meta.id}`;
|
|
869
|
-
if (seen.has(key)) continue;
|
|
870
|
-
seen.add(key);
|
|
871
908
|
const contextCap = caps ? providerContextCap(caps, provider) : undefined;
|
|
909
|
+
const existingIndex = indexByKey.get(key);
|
|
910
|
+
if (existingIndex !== undefined) {
|
|
911
|
+
const existing = out[existingIndex]!;
|
|
912
|
+
const publishedWindow = typeof meta.contextWindow === "number" && meta.contextWindow > 0
|
|
913
|
+
? meta.contextWindow
|
|
914
|
+
: undefined;
|
|
915
|
+
const liveWindow = typeof existing.contextWindow === "number" && existing.contextWindow > 0;
|
|
916
|
+
if (liveWindow || publishedWindow === undefined) continue;
|
|
917
|
+
const seeded: CatalogModel = { ...existing, contextWindow: publishedWindow };
|
|
918
|
+
out[existingIndex] = providers?.[provider]
|
|
919
|
+
? applyProviderConfigHints(
|
|
920
|
+
provider,
|
|
921
|
+
providers[provider],
|
|
922
|
+
seeded,
|
|
923
|
+
contextCap,
|
|
924
|
+
metadataModelIdCaseFoldByProvider?.get(provider),
|
|
925
|
+
)
|
|
926
|
+
: seeded;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
indexByKey.set(key, out.length);
|
|
872
930
|
const model: CatalogModel = {
|
|
873
931
|
provider,
|
|
874
932
|
id: meta.id,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { parseStrictSemver } from "../lib/strict-semver";
|
|
4
|
+
|
|
5
|
+
/** Explicit observation, not runtime selection or authority to run an installer. */
|
|
6
|
+
export interface CodexCliInstallationIdentityInput {
|
|
7
|
+
readonly candidate: string;
|
|
8
|
+
readonly npmPrefix: string;
|
|
9
|
+
readonly npmCli: string;
|
|
10
|
+
readonly node: string;
|
|
11
|
+
/** Where the candidate came from; defaults to an explicit CLI argument. */
|
|
12
|
+
readonly candidateSource?: "explicit-cli" | "selected";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CodexCliInstallationIdentityReport {
|
|
16
|
+
readonly schemaVersion: 1;
|
|
17
|
+
readonly candidateSource: "explicit-cli" | "selected";
|
|
18
|
+
readonly status: "observed" | "refused";
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
readonly installationIdentityObserved: boolean;
|
|
21
|
+
readonly selectionAttested: false;
|
|
22
|
+
readonly managed: false;
|
|
23
|
+
readonly applyAllowed: false;
|
|
24
|
+
readonly packageVersion: string | null;
|
|
25
|
+
readonly npmVersion: string | null;
|
|
26
|
+
readonly identityDigest: string | null;
|
|
27
|
+
readonly proof: "windows-handle-bound" | null;
|
|
28
|
+
readonly toolchain: "observed-only";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InstallationFileRequest {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
readonly maxBytes: number;
|
|
34
|
+
readonly hashOnly?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ObservedInstallationFile {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly identity: {
|
|
40
|
+
readonly volumeSerial: string;
|
|
41
|
+
readonly fileId: string;
|
|
42
|
+
readonly size: number;
|
|
43
|
+
readonly lastWriteTime: string;
|
|
44
|
+
readonly changeTime: string;
|
|
45
|
+
};
|
|
46
|
+
readonly bytes: Uint8Array;
|
|
47
|
+
readonly digest?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type InstallationFilesResult =
|
|
51
|
+
| { readonly kind: "observed"; readonly files: readonly ObservedInstallationFile[] }
|
|
52
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
53
|
+
|
|
54
|
+
export interface CodexCliInstallationIdentityDeps {
|
|
55
|
+
readonly platform?: NodeJS.Platform;
|
|
56
|
+
readonly inspectFiles?: (requests: readonly InstallationFileRequest[]) => Promise<InstallationFilesResult>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const TEXT_LIMIT = 256 * 1024;
|
|
60
|
+
const NODE_LIMIT = 256 * 1024 * 1024;
|
|
61
|
+
const hash = (value: string | Uint8Array): string => createHash("sha256").update(value).digest("hex");
|
|
62
|
+
// NTFS can enable case-sensitive directory entries. Do not collapse different
|
|
63
|
+
// paths merely because ordinary Windows directories usually ignore case.
|
|
64
|
+
const key = (path: string): string => path[0]!.toUpperCase() + path.slice(1);
|
|
65
|
+
|
|
66
|
+
function localPath(value: string): string | null {
|
|
67
|
+
if (typeof value !== "string" || value.length > 30_000 || !/^[a-z]:[\\/]/i.test(value)) return null;
|
|
68
|
+
const path = value.replaceAll("/", "\\");
|
|
69
|
+
const parts = path.slice(3).split("\\");
|
|
70
|
+
if (!parts.length || parts.some(part => !part || part === "." || part === ".."
|
|
71
|
+
|| /[\x00-\x1f<>:"|?*]/.test(part) || /[. ]$/.test(part)
|
|
72
|
+
|| /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) return null;
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function supportedLayoutPath(path: string): boolean {
|
|
77
|
+
// This slice does not adopt package managers or packaged desktop applications.
|
|
78
|
+
return !/(?:^|\\)(?:windowsapps|scoop|\.volta|\.nvm|\.asdf|\.mise|\.fnm|\.nvs|\.nodenv)(?:\\|$)|\.app(?:\\|$)/i.test(path);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function report(
|
|
82
|
+
reason: string,
|
|
83
|
+
versions?: { codex: string; npm: string; digest: string },
|
|
84
|
+
candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
|
|
85
|
+
): CodexCliInstallationIdentityReport {
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
schemaVersion: 1, candidateSource, status: versions ? "observed" : "refused",
|
|
88
|
+
reason, installationIdentityObserved: !!versions, selectionAttested: false, managed: false,
|
|
89
|
+
applyAllowed: false, packageVersion: versions?.codex ?? null, npmVersion: versions?.npm ?? null,
|
|
90
|
+
identityDigest: versions?.digest ?? null, proof: versions ? "windows-handle-bound" : null,
|
|
91
|
+
toolchain: "observed-only",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A refusal before any observation ran, e.g. when no selected candidate can be identified. */
|
|
96
|
+
export function codexCliInstallationRefusal(
|
|
97
|
+
reason: string,
|
|
98
|
+
candidateSource: CodexCliInstallationIdentityReport["candidateSource"] = "explicit-cli",
|
|
99
|
+
): CodexCliInstallationIdentityReport {
|
|
100
|
+
return report(reason, undefined, candidateSource);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function manifest(file: ObservedInstallationFile, name: string, bin: string, expected: string): string | null {
|
|
104
|
+
try {
|
|
105
|
+
const value: unknown = JSON.parse(Buffer.from(file.bytes).toString("utf8"));
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
107
|
+
const item = value as Record<string, unknown>;
|
|
108
|
+
const entry = typeof item.bin === "string" ? item.bin
|
|
109
|
+
: item.bin && typeof item.bin === "object" ? (item.bin as Record<string, unknown>)[bin] : null;
|
|
110
|
+
return item.name === name && typeof item.version === "string" && parseStrictSemver(item.version)
|
|
111
|
+
&& entry === expected ? item.version : null;
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Only the standard npm cmd-shim is linked here; arbitrary wrappers remain unverified. */
|
|
116
|
+
function npmCodexCommandShim(bytes: Uint8Array): boolean {
|
|
117
|
+
const expected = `@ECHO off
|
|
118
|
+
GOTO start
|
|
119
|
+
:find_dp0
|
|
120
|
+
SET dp0=%~dp0
|
|
121
|
+
EXIT /b
|
|
122
|
+
:start
|
|
123
|
+
SETLOCAL
|
|
124
|
+
CALL :find_dp0
|
|
125
|
+
|
|
126
|
+
IF EXIST "%dp0%\\node.exe" (
|
|
127
|
+
SET "_prog=%dp0%\\node.exe"
|
|
128
|
+
) ELSE (
|
|
129
|
+
SET "_prog=node"
|
|
130
|
+
SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\\node_modules\\@openai\\codex\\bin\\codex.js" %*`;
|
|
134
|
+
const normalize = (text: string) => text.replaceAll("\r\n", "\n").trimEnd();
|
|
135
|
+
return normalize(Buffer.from(bytes).toString("utf8")) === normalize(expected);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fileDigest(file: ObservedInstallationFile): string {
|
|
139
|
+
return file.digest ?? hash(file.bytes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fileEvidence(file: ObservedInstallationFile): readonly (string | number)[] {
|
|
143
|
+
const id = file.identity;
|
|
144
|
+
return [key(file.path), id.volumeSerial, id.fileId, id.size, id.lastWriteTime, id.changeTime, fileDigest(file)];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** No filesystem or native-library work occurs until this explicit operation is called. */
|
|
148
|
+
export async function inspectCodexCliInstallationIdentity(
|
|
149
|
+
input: CodexCliInstallationIdentityInput,
|
|
150
|
+
deps: CodexCliInstallationIdentityDeps = {},
|
|
151
|
+
): Promise<CodexCliInstallationIdentityReport> {
|
|
152
|
+
const source = input.candidateSource ?? "explicit-cli";
|
|
153
|
+
const refused = (reason: string) => report(reason, undefined, source);
|
|
154
|
+
if ((deps.platform ?? process.platform) !== "win32") return refused("unsupported_platform");
|
|
155
|
+
const candidate = localPath(input.candidate);
|
|
156
|
+
const prefix = localPath(input.npmPrefix);
|
|
157
|
+
const npmCli = localPath(input.npmCli);
|
|
158
|
+
const node = localPath(input.node);
|
|
159
|
+
if (!candidate || !prefix || !npmCli || !node) return refused("unsafe_path");
|
|
160
|
+
if (![candidate, prefix, npmCli, node].every(supportedLayoutPath)) return refused("unsupported_layout");
|
|
161
|
+
const packageRoot = win32.join(prefix, "node_modules", "@openai", "codex");
|
|
162
|
+
const codexManifest = win32.join(packageRoot, "package.json");
|
|
163
|
+
const codexBin = win32.join(packageRoot, "bin", "codex.js");
|
|
164
|
+
const shim = win32.join(prefix, "codex.cmd");
|
|
165
|
+
// A codex.opencodex-real.cmd backup is npm's renamed launcher: same grammar, same dir.
|
|
166
|
+
const backingShim = win32.join(prefix, "codex.opencodex-real.cmd");
|
|
167
|
+
const acceptedCandidates = source === "selected"
|
|
168
|
+
? [key(codexBin), key(shim), key(backingShim)]
|
|
169
|
+
: [key(codexBin), key(shim)];
|
|
170
|
+
if (!acceptedCandidates.includes(key(candidate))
|
|
171
|
+
|| !/\\node_modules\\npm\\bin\\npm-cli\.js$/i.test(npmCli)
|
|
172
|
+
|| win32.basename(node).toLowerCase() !== "node.exe") return refused("unsupported_layout");
|
|
173
|
+
const npmManifest = win32.join(win32.dirname(win32.dirname(npmCli)), "package.json");
|
|
174
|
+
try {
|
|
175
|
+
const inspect = deps.inspectFiles ?? (await import("./windows-installation-files")).inspectWindowsInstallationFiles;
|
|
176
|
+
// Read both manifests again with every linked file held open: first-pass bytes cannot
|
|
177
|
+
// authorize links after a manifest replacement between the two observations.
|
|
178
|
+
const first = await inspect([{ path: codexManifest, maxBytes: TEXT_LIMIT }, { path: npmManifest, maxBytes: TEXT_LIMIT }]);
|
|
179
|
+
if (first.kind !== "observed") return refused("native_proof_unavailable");
|
|
180
|
+
const initial = new Map(first.files.map(file => [key(file.path), file]));
|
|
181
|
+
const codex = initial.get(key(codexManifest));
|
|
182
|
+
const npm = initial.get(key(npmManifest));
|
|
183
|
+
if (!codex || !npm) return refused("read_failed");
|
|
184
|
+
const codexVersion = manifest(codex, "@openai/codex", "codex", "bin/codex.js");
|
|
185
|
+
const npmVersion = manifest(npm, "npm", "npm", "bin/npm-cli.js");
|
|
186
|
+
if (!codexVersion || !npmVersion) return refused("package_mismatch");
|
|
187
|
+
const requests: InstallationFileRequest[] = [codexManifest, npmManifest, candidate, codexBin, npmCli]
|
|
188
|
+
.filter((path, index, all) => all.findIndex(other => key(other) === key(path)) === index)
|
|
189
|
+
.map(path => ({ path, maxBytes: TEXT_LIMIT }));
|
|
190
|
+
requests.push({ path: node, maxBytes: NODE_LIMIT, hashOnly: true });
|
|
191
|
+
const second = await inspect(requests);
|
|
192
|
+
if (second.kind !== "observed") return refused("native_proof_unavailable");
|
|
193
|
+
const files = new Map(second.files.map(file => [key(file.path), file]));
|
|
194
|
+
if (requests.some(request => !files.has(key(request.path)))) return refused("read_failed");
|
|
195
|
+
for (const before of [codex, npm]) {
|
|
196
|
+
if (JSON.stringify(fileEvidence(before)) !== JSON.stringify(fileEvidence(files.get(key(before.path))!))) {
|
|
197
|
+
return refused("identity_changed");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if ([key(shim), key(backingShim)].includes(key(candidate))
|
|
201
|
+
&& !npmCodexCommandShim(files.get(key(candidate))!.bytes)) {
|
|
202
|
+
return refused("launcher_mismatch");
|
|
203
|
+
}
|
|
204
|
+
const observed = [...files.values()].sort((a, b) => key(a.path) < key(b.path) ? -1 : key(a.path) > key(b.path) ? 1 : 0);
|
|
205
|
+
if (observed.some(file => !/^[a-f0-9]{64}$/i.test(fileDigest(file)))) return refused("read_failed");
|
|
206
|
+
const digest = hash(JSON.stringify(["opencodex-explicit-installation-observation-v1", key(prefix),
|
|
207
|
+
key(candidate), key(npmCli), key(node), ...observed.map(fileEvidence)]));
|
|
208
|
+
return report("identity_observed", { codex: codexVersion, npm: npmVersion, digest }, source);
|
|
209
|
+
} catch { return refused("read_failed"); }
|
|
210
|
+
}
|