@bitkyc08/opencodex 2.57.0 → 2.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -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 {
|
|
@@ -75,6 +77,7 @@ import {
|
|
|
75
77
|
import { finishUpstreamNativeEntry } from "./derive-entry";
|
|
76
78
|
import { finalizeAutoReviewModelOverride } from "./auto-review";
|
|
77
79
|
import { gatedNativeAccountLabel, gatedNativeReauthSuppressionReason, warnGatedNativeSuppressedOnce } from "./gated-native-warn";
|
|
80
|
+
import { reserveCatalogSuppressionReason, warnReserveSuppressedOnce } from "./reserve-warn";
|
|
78
81
|
|
|
79
82
|
interface RetainedCatalogSyncRead {
|
|
80
83
|
readonly catalogPath: string;
|
|
@@ -320,6 +323,11 @@ function writeRetainedCatalogSync({
|
|
|
320
323
|
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
321
324
|
const featured = config.subagentModels ?? [];
|
|
322
325
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
326
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(
|
|
327
|
+
config,
|
|
328
|
+
orderedGoModels,
|
|
329
|
+
catalogModelsForMerge,
|
|
330
|
+
);
|
|
323
331
|
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
324
332
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
325
333
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
@@ -376,6 +384,14 @@ function writeRetainedCatalogSync({
|
|
|
376
384
|
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
377
385
|
const reserveMainSelectors = accountSelectors.filter(selector =>
|
|
378
386
|
isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
|
|
387
|
+
// #4811: an omitted Reserve row carries no reason, so the explanation has to be emitted here,
|
|
388
|
+
// where the selector inputs that produced the omission are still in scope. Silent for every
|
|
389
|
+
// install that did not opt into authless Codex Desktop routing.
|
|
390
|
+
const reserveSuppression = reserveCatalogSuppressionReason(config, {
|
|
391
|
+
includeAccountBoundNativeOpenAi,
|
|
392
|
+
mainSelectors: reserveMainSelectors,
|
|
393
|
+
});
|
|
394
|
+
if (reserveSuppression) warnReserveSuppressedOnce(reserveSuppression);
|
|
379
395
|
// The active file can own a bare source even when the bundled catalog is the build base.
|
|
380
396
|
// A previously clamped qualified projection must not shorten a retained genuine ladder.
|
|
381
397
|
const reserveObservations = [
|
|
@@ -443,6 +459,7 @@ function writeRetainedCatalogSync({
|
|
|
443
459
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
444
460
|
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
445
461
|
const baseline = readNativeBaseline(catalogPath);
|
|
462
|
+
const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models);
|
|
446
463
|
const gatheredProviderNames = new Set(
|
|
447
464
|
Object.entries(config.providers ?? {})
|
|
448
465
|
.filter(([, prov]) => prov.disabled !== true)
|
|
@@ -512,8 +529,10 @@ function writeRetainedCatalogSync({
|
|
|
512
529
|
includeNativeOpenAi,
|
|
513
530
|
accountBoundEntries,
|
|
514
531
|
suppressedBareNativeSlugs,
|
|
532
|
+
suppressedSyntheticMaxSlugs,
|
|
515
533
|
openaiContextCap,
|
|
516
534
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
535
|
+
nativeMultiAgentDefaults: nativePinBaseline,
|
|
517
536
|
policy: {
|
|
518
537
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
519
538
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -850,6 +850,26 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
|
850
850
|
];
|
|
851
851
|
}
|
|
852
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Add generated-registry rows the live provider list did not return, and backfill a
|
|
855
|
+
* published context window onto a live row that arrived without one.
|
|
856
|
+
*
|
|
857
|
+
* The backfill is the reason this function has a merge branch at all. It used to skip every
|
|
858
|
+
* id the live list returned, which is correct for a row that already knows its window and
|
|
859
|
+
* wrong for the normal OpenCode Go case, where discovery returns an id with no context
|
|
860
|
+
* field at all. The serialized Codex catalog was unaffected, because
|
|
861
|
+
* `applyCatalogMetadata` writes `context_window` onto the entry straight from the generated
|
|
862
|
+
* table; the absence was only visible to consumers that read `CatalogModel.contextWindow`
|
|
863
|
+
* (#4971). Those are not cosmetic: `buildClaudeContextWindows` drops a routed row with no
|
|
864
|
+
* window from the map that decides the `[1m]` marker, so a published 1M model could not be
|
|
865
|
+
* recognized as one, and the Grok config writer and several client exporters omit the field
|
|
866
|
+
* entirely rather than emit a known value.
|
|
867
|
+
*
|
|
868
|
+
* It is a missing-value fill, never an override: a live positive window still wins, because
|
|
869
|
+
* the upstream is the authority on its own model. The seeded row is re-hinted so operator
|
|
870
|
+
* precedence is unchanged — a configured window still lowers it and `providerContextCaps`
|
|
871
|
+
* still caps it, exactly as for an appended row.
|
|
872
|
+
*/
|
|
853
873
|
export function augmentRoutedModelsWithMetadata(
|
|
854
874
|
models: CatalogModel[],
|
|
855
875
|
providerNames: string[],
|
|
@@ -858,7 +878,7 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
858
878
|
metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
|
|
859
879
|
): CatalogModel[] {
|
|
860
880
|
const out = [...models];
|
|
861
|
-
const
|
|
881
|
+
const indexByKey = new Map(out.map((model, index) => [`${model.provider}/${model.id}`, index]));
|
|
862
882
|
for (const provider of providerNames) {
|
|
863
883
|
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
864
884
|
if (providers?.[provider]?.liveModels === false) continue;
|
|
@@ -866,9 +886,28 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
866
886
|
if (!jawcodeProvider) continue;
|
|
867
887
|
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
868
888
|
const key = `${provider}/${meta.id}`;
|
|
869
|
-
if (seen.has(key)) continue;
|
|
870
|
-
seen.add(key);
|
|
871
889
|
const contextCap = caps ? providerContextCap(caps, provider) : undefined;
|
|
890
|
+
const existingIndex = indexByKey.get(key);
|
|
891
|
+
if (existingIndex !== undefined) {
|
|
892
|
+
const existing = out[existingIndex]!;
|
|
893
|
+
const publishedWindow = typeof meta.contextWindow === "number" && meta.contextWindow > 0
|
|
894
|
+
? meta.contextWindow
|
|
895
|
+
: undefined;
|
|
896
|
+
const liveWindow = typeof existing.contextWindow === "number" && existing.contextWindow > 0;
|
|
897
|
+
if (liveWindow || publishedWindow === undefined) continue;
|
|
898
|
+
const seeded: CatalogModel = { ...existing, contextWindow: publishedWindow };
|
|
899
|
+
out[existingIndex] = providers?.[provider]
|
|
900
|
+
? applyProviderConfigHints(
|
|
901
|
+
provider,
|
|
902
|
+
providers[provider],
|
|
903
|
+
seeded,
|
|
904
|
+
contextCap,
|
|
905
|
+
metadataModelIdCaseFoldByProvider?.get(provider),
|
|
906
|
+
)
|
|
907
|
+
: seeded;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
indexByKey.set(key, out.length);
|
|
872
911
|
const model: CatalogModel = {
|
|
873
912
|
provider,
|
|
874
913
|
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
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readSync } from "node:fs";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
import { SHIM_MARKER } from "./shim-templates";
|
|
4
|
+
import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The proof-captured launcher snapshot used to identify the selected candidate.
|
|
8
|
+
* Every field is optional because older launchers and direct Bun/source runs
|
|
9
|
+
* supply no snapshot at all; derivation fails closed rather than reading the
|
|
10
|
+
* ambient environment.
|
|
11
|
+
*/
|
|
12
|
+
export interface CodexCliInstallationSnapshot {
|
|
13
|
+
readonly codexCliPath?: string | null;
|
|
14
|
+
readonly path?: string | null;
|
|
15
|
+
readonly pathExt?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CodexCliInstallationTargetRefusal =
|
|
19
|
+
| "unsupported_platform"
|
|
20
|
+
| "candidate_unavailable"
|
|
21
|
+
| "unsupported_layout"
|
|
22
|
+
| "toolchain_unresolved";
|
|
23
|
+
|
|
24
|
+
export type CodexCliInstallationTargetDerivation =
|
|
25
|
+
| { readonly kind: "derived"; readonly input: CodexCliInstallationIdentityInput }
|
|
26
|
+
| { readonly kind: "unavailable"; readonly reason: CodexCliInstallationTargetRefusal };
|
|
27
|
+
|
|
28
|
+
export interface CodexCliInstallationTargetDeps {
|
|
29
|
+
readonly platform?: NodeJS.Platform;
|
|
30
|
+
readonly exists?: (path: string) => boolean;
|
|
31
|
+
/** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
|
|
32
|
+
readonly fileContains?: (path: string, marker: string) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
|
|
36
|
+
const SHIM_PROBE_BYTES = 8 * 1024;
|
|
37
|
+
const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
|
|
38
|
+
|
|
39
|
+
function defaultFileContains(path: string, marker: string): boolean {
|
|
40
|
+
let descriptor: number | undefined;
|
|
41
|
+
let contains = false;
|
|
42
|
+
try {
|
|
43
|
+
descriptor = openSync(path, "r");
|
|
44
|
+
const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
|
|
45
|
+
const count = readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
46
|
+
contains = bytes.subarray(0, count).toString("utf8").includes(marker);
|
|
47
|
+
} catch {
|
|
48
|
+
contains = false;
|
|
49
|
+
} finally {
|
|
50
|
+
if (descriptor !== undefined) {
|
|
51
|
+
try { closeSync(descriptor); } catch { contains = false; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return contains;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* First match wins, mirroring PATH resolution: directories in order, and within
|
|
59
|
+
* each directory every PATHEXT suffix in order (or the exact name when it
|
|
60
|
+
* already carries an extension). Skipping a hit to keep scanning would attest
|
|
61
|
+
* something other than the launcher that actually resolves.
|
|
62
|
+
*/
|
|
63
|
+
function scanPath(
|
|
64
|
+
name: string,
|
|
65
|
+
pathValue: string | null | undefined,
|
|
66
|
+
pathExt: string | null | undefined,
|
|
67
|
+
exists: (path: string) => boolean,
|
|
68
|
+
): string | null {
|
|
69
|
+
const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
|
|
70
|
+
const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
|
|
71
|
+
for (const entry of (pathValue ?? "").split(";")) {
|
|
72
|
+
const dir = entry.trim().replace(/^"+|"+$/g, "");
|
|
73
|
+
if (!dir) continue;
|
|
74
|
+
for (const candidateName of names) {
|
|
75
|
+
const candidate = win32.join(dir, candidateName);
|
|
76
|
+
if (exists(candidate)) return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Derive the four attestation inputs from the proof-captured launcher snapshot.
|
|
84
|
+
* Discovery only proposes candidate paths; the held-handle observation remains
|
|
85
|
+
* the authority, so a stale or racing probe can only produce a refusal, never a
|
|
86
|
+
* false identity. No ambient environment is read: without a snapshot the result
|
|
87
|
+
* is unavailable rather than silently trusting the child's environment.
|
|
88
|
+
*/
|
|
89
|
+
export function deriveCodexCliInstallationInput(
|
|
90
|
+
snapshot: CodexCliInstallationSnapshot,
|
|
91
|
+
deps: CodexCliInstallationTargetDeps = {},
|
|
92
|
+
): CodexCliInstallationTargetDerivation {
|
|
93
|
+
if ((deps.platform ?? process.platform) !== "win32") {
|
|
94
|
+
return { kind: "unavailable", reason: "unsupported_platform" };
|
|
95
|
+
}
|
|
96
|
+
const exists = deps.exists ?? existsSync;
|
|
97
|
+
const fileContains = deps.fileContains ?? defaultFileContains;
|
|
98
|
+
const configured = snapshot.codexCliPath;
|
|
99
|
+
|
|
100
|
+
let candidate: string | null;
|
|
101
|
+
if (configured) {
|
|
102
|
+
if (/^[a-z]:[\\/]/i.test(configured)) {
|
|
103
|
+
if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
104
|
+
candidate = win32.normalize(configured);
|
|
105
|
+
} else {
|
|
106
|
+
if (configured.includes("/") || configured.includes("\\")) {
|
|
107
|
+
return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
108
|
+
}
|
|
109
|
+
candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
|
|
110
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
|
|
114
|
+
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
|
|
118
|
+
// artifact. The renamed original beside it is the file npm wrote.
|
|
119
|
+
if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
|
|
120
|
+
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
|
|
121
|
+
if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
|
|
122
|
+
candidate = backing;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const base = win32.basename(candidate).toLowerCase();
|
|
126
|
+
let prefix: string;
|
|
127
|
+
if (base === "codex.cmd" || base === "codex.opencodex-real.cmd") {
|
|
128
|
+
prefix = win32.dirname(candidate);
|
|
129
|
+
} else if (candidate.toLowerCase().endsWith(CODEX_PACKAGE_SUFFIX)) {
|
|
130
|
+
prefix = candidate.slice(0, candidate.length - CODEX_PACKAGE_SUFFIX.length);
|
|
131
|
+
} else {
|
|
132
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
133
|
+
}
|
|
134
|
+
if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
|
|
135
|
+
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
|
|
139
|
+
let node = win32.join(prefix, "node.exe");
|
|
140
|
+
if (!exists(node)) {
|
|
141
|
+
const resolved = scanPath("node.exe", snapshot.path, null, exists);
|
|
142
|
+
if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
143
|
+
node = resolved;
|
|
144
|
+
}
|
|
145
|
+
const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
|
|
146
|
+
if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
kind: "derived",
|
|
150
|
+
input: Object.freeze({
|
|
151
|
+
candidate,
|
|
152
|
+
npmPrefix: prefix,
|
|
153
|
+
npmCli,
|
|
154
|
+
node,
|
|
155
|
+
candidateSource: "selected",
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
package/src/codex/convergence.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
catalogHasRoutedEntries,
|
|
37
37
|
findSupportedNativeTemplate,
|
|
38
38
|
legacyCatalogBackupPath,
|
|
39
|
+
nativeMultiAgentDefaults,
|
|
39
40
|
parseCatalogJson,
|
|
40
41
|
type RawCatalog,
|
|
41
42
|
type RawEntry,
|
|
@@ -69,6 +70,7 @@ import {
|
|
|
69
70
|
clampCatalogModelsToObservedCodexSupport,
|
|
70
71
|
supportedCodexReasoningEffortsFromObservedCatalog,
|
|
71
72
|
} from "./catalog/effort";
|
|
73
|
+
import { suppressedSyntheticMaxCatalogSlugs } from "./catalog/model-hints";
|
|
72
74
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
73
75
|
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "./account-namespaces";
|
|
74
76
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
@@ -305,6 +307,7 @@ function prepareCatalog(
|
|
|
305
307
|
[catalog.models ?? [], ...nativeRecoverySources],
|
|
306
308
|
);
|
|
307
309
|
const catalogModels = nativeCatalogModels;
|
|
310
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(config, ordered, catalogModels);
|
|
308
311
|
const routedEntries = buildCatalogEntriesFromObservedState({
|
|
309
312
|
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
310
313
|
gptSlugs: [],
|
|
@@ -371,8 +374,10 @@ function prepareCatalog(
|
|
|
371
374
|
includeNativeOpenAi,
|
|
372
375
|
accountBoundEntries,
|
|
373
376
|
suppressedBareNativeSlugs,
|
|
377
|
+
suppressedSyntheticMaxSlugs,
|
|
374
378
|
openaiContextCap,
|
|
375
379
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
380
|
+
nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels),
|
|
376
381
|
policy: {
|
|
377
382
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
378
383
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { shouldSyncCodexOnStart } from "./desired-state";
|
|
3
|
+
import {
|
|
4
|
+
isEffectiveCodexClientCompaction,
|
|
5
|
+
isEffectiveCodexDesktopAuthless,
|
|
6
|
+
} from "./loopback-target";
|
|
7
|
+
|
|
8
|
+
export type CodexDesktopSwitchInertReason =
|
|
9
|
+
| "client_role"
|
|
10
|
+
| "non_loopback_bind_requires_admission_token";
|
|
11
|
+
|
|
12
|
+
export interface CodexDesktopSwitchState {
|
|
13
|
+
stored: boolean;
|
|
14
|
+
effective: boolean;
|
|
15
|
+
inertReason?: CodexDesktopSwitchInertReason;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CodexDesktopSwitchApplyReason =
|
|
19
|
+
| "not_requested"
|
|
20
|
+
| "proxy_not_running"
|
|
21
|
+
| "integration_disabled"
|
|
22
|
+
| "write_lock_busy"
|
|
23
|
+
| "injection_refused";
|
|
24
|
+
|
|
25
|
+
export type CodexDesktopSwitchApply =
|
|
26
|
+
| { applied: true }
|
|
27
|
+
| {
|
|
28
|
+
applied: false;
|
|
29
|
+
reason: CodexDesktopSwitchApplyReason;
|
|
30
|
+
retryable: boolean;
|
|
31
|
+
detail?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface CodexDesktopSwitchReport {
|
|
35
|
+
codexDesktopAuthless: CodexDesktopSwitchState;
|
|
36
|
+
codexClientCompaction: CodexDesktopSwitchState;
|
|
37
|
+
apply: CodexDesktopSwitchApply;
|
|
38
|
+
authSource: { presentsCodexAccount: boolean; summary: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type DesktopSwitchConfig = Pick<
|
|
42
|
+
OcxConfig,
|
|
43
|
+
| "clientIntegrations"
|
|
44
|
+
| "runtimeRole"
|
|
45
|
+
| "hostname"
|
|
46
|
+
| "unauthenticatedLoopbackListener"
|
|
47
|
+
| "codexDesktopAuthless"
|
|
48
|
+
| "codexClientCompaction"
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
function describeSwitch(
|
|
52
|
+
stored: boolean,
|
|
53
|
+
effective: boolean,
|
|
54
|
+
config: Pick<OcxConfig, "runtimeRole">,
|
|
55
|
+
): CodexDesktopSwitchState {
|
|
56
|
+
if (!stored || effective) return { stored, effective };
|
|
57
|
+
return {
|
|
58
|
+
stored,
|
|
59
|
+
effective,
|
|
60
|
+
inertReason: config.runtimeRole === "client"
|
|
61
|
+
? "client_role"
|
|
62
|
+
: "non_loopback_bind_requires_admission_token",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function describeCodexDesktopSwitches(
|
|
67
|
+
config: DesktopSwitchConfig,
|
|
68
|
+
apply: CodexDesktopSwitchApply,
|
|
69
|
+
): CodexDesktopSwitchReport {
|
|
70
|
+
const authlessStored = config.codexDesktopAuthless === true;
|
|
71
|
+
const authlessEffective = isEffectiveCodexDesktopAuthless(config);
|
|
72
|
+
const compactionStored = config.codexClientCompaction === true;
|
|
73
|
+
const compactionEffective = isEffectiveCodexClientCompaction(config);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
codexDesktopAuthless: describeSwitch(authlessStored, authlessEffective, config),
|
|
77
|
+
codexClientCompaction: describeSwitch(compactionStored, compactionEffective, config),
|
|
78
|
+
apply,
|
|
79
|
+
authSource: authlessEffective
|
|
80
|
+
? {
|
|
81
|
+
presentsCodexAccount: false,
|
|
82
|
+
summary: "The Codex app will not require its own account sign-in.",
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
presentsCodexAccount: true,
|
|
86
|
+
summary: "The Codex app will require its own account sign-in.",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function applyCodexDesktopSwitches(
|
|
92
|
+
config: OcxConfig,
|
|
93
|
+
): Promise<CodexDesktopSwitchApply> {
|
|
94
|
+
if (!shouldSyncCodexOnStart(config)) {
|
|
95
|
+
return { applied: false, reason: "integration_disabled", retryable: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { readRuntimePort } = await import("../config/process-state");
|
|
99
|
+
const runtime = readRuntimePort(process.pid);
|
|
100
|
+
if (!runtime) {
|
|
101
|
+
return { applied: false, reason: "proxy_not_running", retryable: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Imported at call time, not module load. The settings route reaches this module on
|
|
106
|
+
// every GET, and pulling the whole injection graph in just to report stored-versus-
|
|
107
|
+
// effective state would put it on a read path that never writes anything.
|
|
108
|
+
const { injectCodexConfig } = await import("./inject");
|
|
109
|
+
const result = await injectCodexConfig(runtime.port, config);
|
|
110
|
+
if (result.status === "skipped") {
|
|
111
|
+
return {
|
|
112
|
+
applied: false,
|
|
113
|
+
reason: "integration_disabled",
|
|
114
|
+
retryable: false,
|
|
115
|
+
detail: result.message,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (result.success) {
|
|
119
|
+
// history_paginated_requires_native_writer stands down only the legacy relabel;
|
|
120
|
+
// apply still writes the routing and catalog half for paginated Codex homes.
|
|
121
|
+
return { applied: true };
|
|
122
|
+
}
|
|
123
|
+
if (result.retryable === true) {
|
|
124
|
+
return {
|
|
125
|
+
applied: false,
|
|
126
|
+
reason: "write_lock_busy",
|
|
127
|
+
retryable: true,
|
|
128
|
+
detail: result.message,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
applied: false,
|
|
133
|
+
reason: "injection_refused",
|
|
134
|
+
retryable: false,
|
|
135
|
+
detail: result.message,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
applied: false,
|
|
140
|
+
reason: "injection_refused",
|
|
141
|
+
retryable: false,
|
|
142
|
+
detail: error instanceof Error ? error.message : "Codex config injection failed.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|