@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VendorContextInference } from "./vendor-context";
|
|
1
2
|
import { isCodexControlPlaneModel } from "../control-plane-models";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -120,6 +121,8 @@ export interface CatalogModel {
|
|
|
120
121
|
suppressSyntheticMax?: boolean;
|
|
121
122
|
defaultReasoningEffort?: string;
|
|
122
123
|
contextWindow?: number;
|
|
124
|
+
/** Bundled fallback only; never a serving-provider capability assertion. */
|
|
125
|
+
contextWindowInference?: VendorContextInference;
|
|
123
126
|
maxInputTokens?: number;
|
|
124
127
|
/** Model-scoped output-token ceiling; omitted when no authoritative value is known. */
|
|
125
128
|
maxOutputTokens?: number;
|
|
@@ -139,6 +142,8 @@ export interface CatalogModel {
|
|
|
139
142
|
supportsVerbosity?: boolean;
|
|
140
143
|
/** Whether this exact routed model has a verified OpenAI-compatible service tier. */
|
|
141
144
|
supportsServiceTier?: boolean;
|
|
145
|
+
/** Captured eligible service-tier wire explicitly maps ultrafast to ultrafast. */
|
|
146
|
+
supportsUltraFastTier?: boolean;
|
|
142
147
|
/** Optional provider-specific copy for the advertised Fast tier. */
|
|
143
148
|
fastTierDescription?: string;
|
|
144
149
|
supportsReasoningSummaries?: boolean;
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import type { FastPolicyAuthority } from "../../providers/fastwire";
|
|
54
54
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
|
|
55
55
|
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
56
|
+
import { recordAntigravityAccountRoster } from "../../providers/antigravity-account-roster";
|
|
56
57
|
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
57
58
|
import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
|
|
58
59
|
import { effectiveModelAliases } from "../../providers/default-aliases";
|
|
@@ -807,6 +808,9 @@ export async function fetchProviderModelsWithAuth(
|
|
|
807
808
|
provider: name,
|
|
808
809
|
cacheGeneration,
|
|
809
810
|
});
|
|
811
|
+
// Entitlement is per account (Claude 4.6 vs 5.5, Argon): remember what THIS account
|
|
812
|
+
// lists so account selection can prefer one that serves the requested model.
|
|
813
|
+
if (auth.oauthAccountId) recordAntigravityAccountRoster(name, auth.oauthAccountId, antigravity);
|
|
810
814
|
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
811
815
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
812
816
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
|
|
2
|
+
import { configuredUltraFastSlugs } from "./configured-tiers";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { initializeConfigOwnership } from "../../lib/config-ownership";
|
|
@@ -509,6 +510,7 @@ function writeRetainedCatalogSync({
|
|
|
509
510
|
: [];
|
|
510
511
|
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
511
512
|
ultraFastTier: ultraFastTierEnabled(config),
|
|
513
|
+
configuredUltraFastSlugs: configuredUltraFastSlugs(orderedGoModels),
|
|
512
514
|
modelPickerOrder,
|
|
513
515
|
accountSelectors,
|
|
514
516
|
catalogModels: catalogModelsForMerge,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { canInferVendorContext } from "./vendor-context";
|
|
1
2
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
3
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
4
|
+
import { supportsConfiguredUltraFast } from "./configured-tiers";
|
|
3
5
|
import { execFileSync } from "node:child_process";
|
|
4
6
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
5
7
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -318,6 +320,7 @@ function boundCustomNativeReasoning(
|
|
|
318
320
|
return bounded;
|
|
319
321
|
}
|
|
320
322
|
|
|
323
|
+
/** Gather discovered and custom rows under one captured provider policy, including speed capabilities. */
|
|
321
324
|
async function gatherRoutedModelsUncached(
|
|
322
325
|
config: OcxConfig,
|
|
323
326
|
capture: GatherFlightCapture,
|
|
@@ -358,6 +361,7 @@ async function gatherRoutedModelsUncached(
|
|
|
358
361
|
const policy = fastPolicyForModel(apiProvider.provider, model.id, apiProvider.name);
|
|
359
362
|
const supported = serviceTierSupportFromPolicy(policy);
|
|
360
363
|
if (supported !== undefined) model.supportsServiceTier = supported;
|
|
364
|
+
model.supportsUltraFastTier = supportsConfiguredUltraFast(policy);
|
|
361
365
|
if (supported === true && policy.fastTierDescription !== undefined) model.fastTierDescription = policy.fastTierDescription;
|
|
362
366
|
}
|
|
363
367
|
}
|
|
@@ -467,7 +471,7 @@ async function gatherRoutedModelsUncached(
|
|
|
467
471
|
...inherited,
|
|
468
472
|
catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
469
473
|
...(typeof custom.contextWindow === "number" && custom.contextWindow > 0
|
|
470
|
-
? { contextWindow: custom.contextWindow }
|
|
474
|
+
? { contextWindow: custom.contextWindow, contextWindowInference: undefined }
|
|
471
475
|
: {}),
|
|
472
476
|
...(Array.isArray(custom.inputModalities)
|
|
473
477
|
? { inputModalities: [...custom.inputModalities] }
|
|
@@ -654,6 +658,7 @@ async function gatherRoutedModelsUncached(
|
|
|
654
658
|
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
655
659
|
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
656
660
|
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
661
|
+
supportsUltraFastTier: supportsConfiguredUltraFast(fastPolicy),
|
|
657
662
|
...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined
|
|
658
663
|
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
659
664
|
: {}),
|
|
@@ -686,7 +691,8 @@ async function gatherRoutedModelsUncached(
|
|
|
686
691
|
: undefined;
|
|
687
692
|
const merged: CatalogModel = replaced ? {
|
|
688
693
|
...base,
|
|
689
|
-
...(base.contextWindow === undefined && replaced.contextWindow !== undefined
|
|
694
|
+
...(base.contextWindow === undefined && replaced.contextWindow !== undefined
|
|
695
|
+
? { contextWindow: replaced.contextWindow, ...(replaced.contextWindowInference ? { contextWindowInference: replaced.contextWindowInference, contextCap: replaced.contextCap, contextCapped: replaced.contextCapped } : {}) } : {}),
|
|
690
696
|
...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
|
|
691
697
|
...(mergedMaxOutput !== undefined ? { maxOutputTokens: mergedMaxOutput } : {}),
|
|
692
698
|
...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
|
|
@@ -703,17 +709,29 @@ async function gatherRoutedModelsUncached(
|
|
|
703
709
|
...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
|
|
704
710
|
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
705
711
|
} : base;
|
|
712
|
+
// A custom-only model may never appear in /models or the provider's configured roster.
|
|
713
|
+
// Fill only its absent context; a full hint overlay would overwrite custom capabilities.
|
|
714
|
+
const customContextHints = merged.contextWindow === undefined && effectiveProvider && canInferVendorContext(cm.provider, effectiveProvider)
|
|
715
|
+
? applyProviderConfigHints(cm.provider, effectiveProvider, { provider: cm.provider, id: cm.modelId },
|
|
716
|
+
providerContextCap(config, cm.provider), metadataModelIdCaseFoldByProvider.get(cm.provider),
|
|
717
|
+
undefined, metadataConfigDirByProvider.get(cm.provider))
|
|
718
|
+
: undefined;
|
|
719
|
+
const contextResolved: CatalogModel = customContextHints?.contextWindow !== undefined
|
|
720
|
+
? { ...merged, contextWindow: customContextHints.contextWindow,
|
|
721
|
+
...(customContextHints.contextWindowInference ? { contextWindowInference: customContextHints.contextWindowInference } : {}),
|
|
722
|
+
...(customContextHints.contextCap !== undefined ? { contextCap: customContextHints.contextCap, contextCapped: customContextHints.contextCapped } : {}) }
|
|
723
|
+
: merged;
|
|
706
724
|
// Catalog-advertised efforts are bounded whenever the model id is a pinned native
|
|
707
725
|
// slug. Desktop validates that id, so a gateway such as YYLJ/gpt-6-astra still cannot
|
|
708
726
|
// advertise none/minimal. Full native identity stays behind the alias predicate.
|
|
709
727
|
const nativeEffortSource = hasNativeOpenAiCapabilityMetadata(cm.modelId);
|
|
710
728
|
const reasoningBounded = nativeEffortSource
|
|
711
729
|
? boundCustomNativeReasoning(
|
|
712
|
-
|
|
730
|
+
contextResolved,
|
|
713
731
|
nativeReasoningEfforts(cm.modelId),
|
|
714
732
|
nativeAliasDefaultEffort ?? nativeDefaultReasoningEffort(cm.modelId),
|
|
715
733
|
)
|
|
716
|
-
:
|
|
734
|
+
: contextResolved;
|
|
717
735
|
// Vision-sidecar coverage only: when the enriched provider's shared predicate matches
|
|
718
736
|
// noVisionModels or text-without-image modelInputModalities, advertise image input so the
|
|
719
737
|
// Codex app lets images reach the sidecar (#349/#344). Deliberately NOT the full
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
|
|
2
|
+
import { PROVIDER_REGISTRY } from "../../providers/registry";
|
|
3
|
+
import type { OcxProviderConfig } from "../../types";
|
|
4
|
+
|
|
5
|
+
/** A bundled model-window estimate, not evidence of the serving gateway's capacity. */
|
|
6
|
+
export interface VendorContextInference {
|
|
7
|
+
source: "vendor" | "aggregator";
|
|
8
|
+
provider: string;
|
|
9
|
+
modelId: string;
|
|
10
|
+
contextWindow: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Capacity and price have different authorities: hosting vendors (Cerebras/Bedrock), Azure
|
|
14
|
+
// and the Codex subscription table must not replace the original vendor's API window.
|
|
15
|
+
const VENDORS = ["anthropic", "openai", "google", "moonshot", "minimax", "deepseek", "xai", "zai", "mistral", "xiaomi"];
|
|
16
|
+
const AGGREGATORS = ["openrouter", "opencode-go"];
|
|
17
|
+
// Snapshot names once, never consult the mutable registry after gather flight admission.
|
|
18
|
+
const BUILTIN_NAMES = new Set(PROVIDER_REGISTRY.map(entry => entry.id.toLowerCase()));
|
|
19
|
+
/**
|
|
20
|
+
* Opt-in only (#1073): an id-only custom row keeps the conservative fallback unless the
|
|
21
|
+
* operator sets `inferContextWindow: true`. Built-in names stay excluded even when set.
|
|
22
|
+
*/
|
|
23
|
+
export function canInferVendorContext(provider: string, config: Pick<OcxProviderConfig, "inferContextWindow"> | undefined): boolean {
|
|
24
|
+
return config?.inferContextWindow === true
|
|
25
|
+
&& !BUILTIN_NAMES.has(provider.toLowerCase()) && !resolveMetadataProvider(provider);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalize = (id: string): string => id.toLowerCase().replaceAll(".", "-");
|
|
29
|
+
type Candidate = VendorContextInference | null;
|
|
30
|
+
|
|
31
|
+
/** Build a pure lookup; conflicting windows within one authority tier fail closed. */
|
|
32
|
+
export function createVendorContextLookup(
|
|
33
|
+
vendors: readonly ModelMetadata[],
|
|
34
|
+
aggregators: readonly ModelMetadata[],
|
|
35
|
+
): (id: string) => VendorContextInference | undefined {
|
|
36
|
+
const build = (rows: readonly ModelMetadata[], source: VendorContextInference["source"]) => {
|
|
37
|
+
const exact = new Map<string, Candidate>();
|
|
38
|
+
const normalized = new Map<string, Candidate>();
|
|
39
|
+
const add = (map: Map<string, Candidate>, id: string, value: VendorContextInference) => {
|
|
40
|
+
if (!map.has(id)) map.set(id, value);
|
|
41
|
+
else if (map.get(id)?.contextWindow !== value.contextWindow) map.set(id, null);
|
|
42
|
+
};
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
const window = row.contextWindow;
|
|
45
|
+
if (!Number.isSafeInteger(window) || window! <= 0 || row.id.startsWith("~")) continue;
|
|
46
|
+
const value: VendorContextInference = { source, provider: row.provider, modelId: row.id, contextWindow: window! };
|
|
47
|
+
const ids = source === "aggregator" ? [row.id, row.id.slice(row.id.lastIndexOf("/") + 1)] : [row.id];
|
|
48
|
+
for (const id of ids) {
|
|
49
|
+
if (!id) continue;
|
|
50
|
+
add(exact, id, value);
|
|
51
|
+
add(normalized, normalize(id), value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { exact, normalized };
|
|
55
|
+
};
|
|
56
|
+
const tiers = [build(vendors, "vendor"), build(aggregators, "aggregator")];
|
|
57
|
+
return rawId => {
|
|
58
|
+
const id = rawId.trim();
|
|
59
|
+
if (!id || id.startsWith("~")) return undefined;
|
|
60
|
+
for (const tier of tiers) {
|
|
61
|
+
// An exact spelling wins over its normalized alias. A conflicting key does not
|
|
62
|
+
// fall through to a lower-authority source and accidentally regain a larger window.
|
|
63
|
+
const map = tier.exact.has(id) ? tier.exact : tier.normalized;
|
|
64
|
+
const key = map === tier.exact ? id : normalize(id);
|
|
65
|
+
if (map.has(key)) {
|
|
66
|
+
const value = map.get(key);
|
|
67
|
+
return value ? { ...value } : undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let lookup: ReturnType<typeof createVendorContextLookup> | undefined;
|
|
75
|
+
export function vendorContextInference(modelId: string): VendorContextInference | undefined {
|
|
76
|
+
lookup ??= createVendorContextLookup(VENDORS.flatMap(listModelMetadata), AGGREGATORS.flatMap(listModelMetadata));
|
|
77
|
+
return lookup(modelId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function vendorContextWindow(modelId: string): number | undefined {
|
|
81
|
+
return vendorContextInference(modelId)?.contextWindow;
|
|
82
|
+
}
|
|
@@ -166,15 +166,21 @@ function canonicalFileIdentity(canonical: string): string | null {
|
|
|
166
166
|
/** Capture before the first write; nested native children share the same recovery scope. */
|
|
167
167
|
export function withConfigWriteRecovery<T>(
|
|
168
168
|
configPath: string, held: LockHandle, run: () => T,
|
|
169
|
-
options: { refreshBeforeSpawn?: boolean; preimage?: Buffer } = {},
|
|
169
|
+
options: { refreshBeforeSpawn?: boolean; preimage?: Buffer; requireMatchingPreimage?: boolean } = {},
|
|
170
170
|
): T {
|
|
171
|
-
|
|
171
|
+
const nested = recoveries.get(held);
|
|
172
|
+
if (nested && !options.requireMatchingPreimage) return run();
|
|
172
173
|
let canonical: string;
|
|
173
174
|
try { canonical = assertConfigWriteDestination(configPath, held); }
|
|
174
175
|
catch { throw new ConfigWriteDestinationChanged(); }
|
|
175
176
|
const identity = canonicalFileIdentity(canonical);
|
|
176
177
|
const preimage = identity === null ? null : readFileSync(canonical);
|
|
177
178
|
if (canonicalFileIdentity(canonical) !== identity) throw new ConfigWriteDestinationChanged();
|
|
179
|
+
if (options.requireMatchingPreimage && (!options.preimage || !preimage?.equals(options.preimage))) {
|
|
180
|
+
throw new ConfigWriteDestinationChanged();
|
|
181
|
+
}
|
|
182
|
+
if (nested && (nested.canonical !== canonical || nested.identity !== identity)) throw new ConfigWriteDestinationChanged();
|
|
183
|
+
if (nested) return run();
|
|
178
184
|
const recovery: ConfigWriteRecovery = {
|
|
179
185
|
canonical, identity, preimage: options.preimage ?? preimage,
|
|
180
186
|
refreshBeforeSpawn: options.refreshBeforeSpawn === true && options.preimage === undefined,
|
|
@@ -317,6 +323,38 @@ export function publishConfigWriteTarget<T>(
|
|
|
317
323
|
});
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
export function removeConfigWriteEntry(configPath: string, held: LockHandle, path: string): void {
|
|
327
|
+
assertConfigWriteDestination(configPath, held);
|
|
328
|
+
watchConfigWriteTargets(held, [path]);
|
|
329
|
+
const watched = targets.get(held)!;
|
|
330
|
+
const expected = watched.get(path)!;
|
|
331
|
+
const current = destination(path);
|
|
332
|
+
if (expected.canonical !== current.canonical || expected.inode !== current.inode
|
|
333
|
+
|| current.canonical !== join(realpathSync.native(dirname(resolve(path))), basename(resolve(path)))) {
|
|
334
|
+
throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
|
|
335
|
+
}
|
|
336
|
+
if (expected.inode === null) return;
|
|
337
|
+
|
|
338
|
+
const entry = join(realpathSync.native(dirname(resolve(path))), basename(resolve(path)));
|
|
339
|
+
const identity = (file: { dev: bigint; ino: bigint }) => `${file.dev}:${file.ino}`;
|
|
340
|
+
const before = lstatSync(entry, { bigint: true });
|
|
341
|
+
if (before.isSymbolicLink() || !before.isFile() || identity(before) !== expected.inode) {
|
|
342
|
+
throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
|
|
343
|
+
}
|
|
344
|
+
assertConfigWriteDestination(configPath, held);
|
|
345
|
+
const now = lstatSync(entry, { bigint: true });
|
|
346
|
+
if (now.isSymbolicLink() || !now.isFile() || identity(now) !== expected.inode) {
|
|
347
|
+
throw new Error("Codex artifact destination changed; files and recovery evidence were preserved.");
|
|
348
|
+
}
|
|
349
|
+
unlinkSync(entry);
|
|
350
|
+
assertConfigWriteDestination(configPath, held);
|
|
351
|
+
const after = destination(path);
|
|
352
|
+
if (after.inode !== null || after.canonical !== expected.canonical) {
|
|
353
|
+
throw new Error("Codex artifact destination changed; recovery evidence was preserved.");
|
|
354
|
+
}
|
|
355
|
+
watched.set(path, after);
|
|
356
|
+
}
|
|
357
|
+
|
|
320
358
|
export function configWriteLockFailureMessage(failure: Exclude<ConfigWriteAcquireResult, { ok: true }>): string {
|
|
321
359
|
if (failure.error === "missing-parent") return `Codex configuration parent is missing or unresolved at ${failure.detail}; no files were changed.`;
|
|
322
360
|
return failure.error === "unsafe"
|
package/src/codex/convergence.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
|
|
2
|
+
import { configuredUltraFastSlugs } from "./catalog/configured-tiers";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { sameCatalogHealPath } from "./catalog/heal-observation";
|
|
4
5
|
|
|
@@ -371,6 +372,7 @@ function prepareCatalog(
|
|
|
371
372
|
);
|
|
372
373
|
const mergedModels = mergeCatalogEntriesFromObservedState({
|
|
373
374
|
ultraFastTier: ultraFastTierEnabled(config),
|
|
375
|
+
configuredUltraFastSlugs: configuredUltraFastSlugs(ordered),
|
|
374
376
|
modelPickerOrder,
|
|
375
377
|
accountSelectors,
|
|
376
378
|
catalogModels,
|
|
@@ -51,6 +51,8 @@ export interface DesktopProcess {
|
|
|
51
51
|
createdAt: string;
|
|
52
52
|
/** Absolute executable path, used for membership and the shell predicate. */
|
|
53
53
|
executable: string;
|
|
54
|
+
/** Windows-only private launch context; never included in restart results or logs. */
|
|
55
|
+
commandLine?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface DesktopAppAdapter {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { sep, win32 } from "node:path";
|
|
15
15
|
import { resolveTrustedWindowsPowerShellExe, resolveTrustedWindowsTaskkillExe } from "../../lib/windows-elevation";
|
|
16
|
+
import { activateWindowsCodexCompatibility, assertWindowsCompatibilityContext, captureWindowsCompatibilityContext } from "../desktop-compatibility/windows-package-command";
|
|
16
17
|
import {
|
|
17
18
|
isUnderRoot,
|
|
18
19
|
type DesktopAppAdapter,
|
|
@@ -50,8 +51,7 @@ function isMemberExecutable(executable: string, root: string): boolean {
|
|
|
50
51
|
* changes between builds, so a literal AUMID would silently stop matching and
|
|
51
52
|
* then either do nothing or — worse — match a package we did not mean.
|
|
52
53
|
*/
|
|
53
|
-
|
|
54
|
-
const script = [
|
|
54
|
+
export const WINDOWS_PACKAGE_DISCOVERY_SCRIPT = [
|
|
55
55
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
56
56
|
"Import-Module Appx -ErrorAction SilentlyContinue",
|
|
57
57
|
"$p = Get-AppxPackage -Name OpenAI.Codex",
|
|
@@ -60,12 +60,8 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
|
|
|
60
60
|
" $p.PackageFamilyName; $p.InstallLocation; \"$($p.PackageFamilyName)!App\"",
|
|
61
61
|
"}",
|
|
62
62
|
].join("; ");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", script], POWERSHELL_PROBE_OPTIONS);
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
63
|
+
|
|
64
|
+
export function parseWindowsDesktopPackage(stdout: string): DesktopAppInstall | null {
|
|
69
65
|
const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
|
70
66
|
if (lines.length < 3 || lines[0] === "MISS") return null;
|
|
71
67
|
const [family, installLocation, aumid] = lines;
|
|
@@ -73,6 +69,16 @@ function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
|
|
|
73
69
|
return { id: family, root: installLocation, relaunch: aumid };
|
|
74
70
|
}
|
|
75
71
|
|
|
72
|
+
function discoverPackage(exec: DesktopExec): DesktopAppInstall | null {
|
|
73
|
+
let stdout: string;
|
|
74
|
+
try {
|
|
75
|
+
stdout = exec(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_PACKAGE_DISCOVERY_SCRIPT], POWERSHELL_PROBE_OPTIONS);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return parseWindowsDesktopPackage(stdout);
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
/**
|
|
77
83
|
* Only `ChatGPT.exe` processes whose image lives under the discovered install
|
|
78
84
|
* location AND owned by the current user. The install location alone is not
|
|
@@ -100,7 +106,8 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
|
|
|
100
106
|
" if ($o -and $o.ReturnValue -eq 0 -and $o.User) {",
|
|
101
107
|
" $owner = if ($o.Domain) { \"$($o.Domain)\\$($o.User)\" } else { $o.User }",
|
|
102
108
|
" if ($owner -ieq $me) {",
|
|
103
|
-
"
|
|
109
|
+
" $encoded=[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_.CommandLine))",
|
|
110
|
+
" \"$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.ToString('o')) $($_.ExecutablePath)`t$encoded\"",
|
|
104
111
|
" }",
|
|
105
112
|
" }",
|
|
106
113
|
" }",
|
|
@@ -126,7 +133,9 @@ function listPackageProcesses(exec: DesktopExec, install: DesktopAppInstall): De
|
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
function parseProcessLine(line: string, root: string): DesktopProcess | null {
|
|
129
|
-
const
|
|
136
|
+
const [listing, encoded, ...extra] = line.split("\t");
|
|
137
|
+
if (extra.length || (encoded && (encoded.length > 65_536 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)))) return null;
|
|
138
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)(?:\s+(.+))?$/.exec(listing ?? "");
|
|
130
139
|
if (!match) return null;
|
|
131
140
|
const pid = Number(match[1]);
|
|
132
141
|
const parentPid = Number(match[2]);
|
|
@@ -143,7 +152,8 @@ function parseProcessLine(line: string, root: string): DesktopProcess | null {
|
|
|
143
152
|
// Authoritative membership. PowerShell StartsWith already cheap-filtered, but
|
|
144
153
|
// that test is a string prefix and is how a sibling install would sneak in.
|
|
145
154
|
if (!isMemberExecutable(executable, root)) return null;
|
|
146
|
-
return { pid, parentPid, createdAt, executable
|
|
155
|
+
return { pid, parentPid, createdAt, executable,
|
|
156
|
+
...(encoded !== undefined ? { commandLine: Buffer.from(encoded, "base64").toString("utf8") } : {}) };
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
/**
|
|
@@ -215,13 +225,16 @@ export const windowsDesktopAppAdapter: DesktopAppAdapter = {
|
|
|
215
225
|
exec(resolveTrustedWindowsTaskkillExe(), ["/PID", String(root.pid), "/T", "/F"], POWERSHELL_PROBE_OPTIONS);
|
|
216
226
|
},
|
|
217
227
|
|
|
218
|
-
captureRelaunchContext(): Record<string, string> {
|
|
219
|
-
|
|
220
|
-
// carrying forward.
|
|
221
|
-
return {};
|
|
228
|
+
captureRelaunchContext(_exec, _install, processes): Record<string, string> {
|
|
229
|
+
return captureWindowsCompatibilityContext(processes);
|
|
222
230
|
},
|
|
223
231
|
|
|
224
|
-
relaunch(exec, install): void {
|
|
232
|
+
relaunch(exec, install, context): void {
|
|
233
|
+
if (context.codexCompatibilityPacUrl) {
|
|
234
|
+
assertWindowsCompatibilityContext(context);
|
|
235
|
+
activateWindowsCodexCompatibility(exec, install, context.codexCompatibilityPacUrl);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
225
238
|
// Throws on failure so the ladder reports relaunch_failed. The old code
|
|
226
239
|
// returned targets_survived here, which was dishonest: everything HAD died
|
|
227
240
|
// and it was the relaunch that failed.
|
|
@@ -89,6 +89,7 @@ export type DesktopAppRestartReason =
|
|
|
89
89
|
| "self_ancestry"
|
|
90
90
|
| "restart_in_flight"
|
|
91
91
|
| "handoff_started"
|
|
92
|
+
| "relaunch_context_failed"
|
|
92
93
|
| "targets_survived"
|
|
93
94
|
| "relaunch_failed";
|
|
94
95
|
|
|
@@ -263,7 +264,9 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
|
|
|
263
264
|
// graphical session variables, and after termination there is nothing to read them
|
|
264
265
|
// from. Ordering this wrongly works on macOS and Windows and produces a Linux app
|
|
265
266
|
// that cannot reach the compositor.
|
|
266
|
-
|
|
267
|
+
let context: Record<string, string>;
|
|
268
|
+
try { context = adapter.captureRelaunchContext(exec, install, processes); }
|
|
269
|
+
catch { return skipped("relaunch_context_failed"); }
|
|
267
270
|
|
|
268
271
|
const isAlive = io.isAlive ?? defaultIsAlive;
|
|
269
272
|
const sleep = io.sleep ?? defaultSleep;
|
|
@@ -345,4 +348,3 @@ export function restartCodexDesktopApp(io: DesktopAppRestartIo = {}): DesktopApp
|
|
|
345
348
|
if (!handedOff) releaseDesktopRestartLock(io.lock);
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
|
-
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { getConfigDir } from "../../config/paths";
|
|
3
|
+
import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
|
|
4
|
+
import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority, type DesktopAuthorityInspection, type StoredDesktopAuthority } from "./certificate-store";
|
|
5
|
+
import { createWindowsCertificateTrust, inspectWindowsCertificateTrust, type DesktopCertificateTrust } from "./windows-certificate-trust";
|
|
6
|
+
import { acquireDesktopCertificateMutation, desktopCompatibilityRuntimeActive } from "./runtime-ownership";
|
|
7
|
+
|
|
8
|
+
export interface DesktopCertificateStatus {
|
|
9
|
+
supported: boolean;
|
|
10
|
+
state: "missing" | "invalid" | "expired" | "renewal-required" | "prepared" | "trusted" | "unknown";
|
|
11
|
+
fingerprint?: string;
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
renewalDue?: boolean;
|
|
14
|
+
trust?: DesktopCertificateTrust;
|
|
15
|
+
busy: "prepare" | "trust" | "remove-trust" | "renew" | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DesktopCertificateServiceIo {
|
|
19
|
+
platform?: string;
|
|
20
|
+
inspect?: () => DesktopAuthorityInspection;
|
|
21
|
+
prepare?: () => Promise<StoredDesktopAuthority>;
|
|
22
|
+
load?: (allowExpiredForRemoval: boolean) => Promise<StoredDesktopAuthority>;
|
|
23
|
+
renew?: (fingerprint: string) => Promise<StoredDesktopAuthority>;
|
|
24
|
+
readTrust?: (authority: Extract<DesktopAuthorityInspection, { fingerprint: string }>) => Promise<DesktopCertificateTrust>;
|
|
25
|
+
changeTrust?: (authority: StoredDesktopAuthority, action: "trust" | "remove") => Promise<DesktopCertificateTrust>;
|
|
26
|
+
appRunning?: () => Promise<boolean | null>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class DesktopCertificateServiceError extends Error {
|
|
30
|
+
constructor(readonly code: "unsupported" | "busy" | "not_prepared" | "fingerprint_changed" | "app_running" | "runtime_running" | "app_state_unknown" | "trust_unknown" | "trust_not_applied") {
|
|
31
|
+
super(`desktop_compatibility_${code}`); this.name = "DesktopCertificateServiceError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** This service owns setup only: no proxy listeners, app restart, account or quota writes. */
|
|
36
|
+
export function createDesktopCertificateService(directory = join(getConfigDir(), "codex-desktop-compatibility"), io: DesktopCertificateServiceIo = {}) {
|
|
37
|
+
const platform = io.platform ?? process.platform;
|
|
38
|
+
const inspect = io.inspect ?? (() => inspectDesktopCompatibilityAuthority(directory));
|
|
39
|
+
const prepare = io.prepare ?? (() => ensureDesktopCompatibilityAuthority({ directory }));
|
|
40
|
+
const load = io.load ?? (allowExpired => loadDesktopCompatibilityAuthority({ directory }, allowExpired));
|
|
41
|
+
const renew = io.renew ?? (fingerprint => renewDesktopCompatibilityAuthority({ directory }, fingerprint));
|
|
42
|
+
const readTrust = io.readTrust ?? (value => inspectWindowsCertificateTrust(value.certPem, value.fingerprint));
|
|
43
|
+
const changeTrust = io.changeTrust ?? ((authority, action) => createWindowsCertificateTrust(authority, authority.fingerprint)[action]());
|
|
44
|
+
const appRunning = io.appRunning ?? (async () => {
|
|
45
|
+
const install = windowsDesktopAppAdapter.discover(windowsDefaultExec);
|
|
46
|
+
if (!install) return null; // Unknown installation is not proof that no app uses the authority.
|
|
47
|
+
const processes = windowsDesktopAppAdapter.listProcesses(windowsDefaultExec, install);
|
|
48
|
+
return processes === null ? null : processes.length > 0;
|
|
49
|
+
});
|
|
50
|
+
let busy: DesktopCertificateStatus["busy"] = null;
|
|
51
|
+
async function status(): Promise<DesktopCertificateStatus> {
|
|
52
|
+
if (platform !== "win32") return { supported: false, state: "missing", busy };
|
|
53
|
+
const stored = inspect();
|
|
54
|
+
if (!("fingerprint" in stored)) return { supported: true, state: stored.status, busy };
|
|
55
|
+
const trusted = await readTrust(stored).catch(() => "unknown" as const);
|
|
56
|
+
return { supported: true, state: stored.status === "expired" ? "expired"
|
|
57
|
+
: stored.status === "renewal-required" ? "renewal-required"
|
|
58
|
+
: trusted === "trusted" ? "trusted" : trusted === "not-trusted" ? "prepared" : "unknown",
|
|
59
|
+
fingerprint: stored.fingerprint, expiresAt: stored.expiresAt, renewalDue: stored.renewalDue, trust: trusted, busy };
|
|
60
|
+
}
|
|
61
|
+
async function action(kind: NonNullable<DesktopCertificateStatus["busy"]>, expectedFingerprint?: string): Promise<DesktopCertificateStatus> {
|
|
62
|
+
if (platform !== "win32") throw new DesktopCertificateServiceError("unsupported");
|
|
63
|
+
if (busy !== null) throw new DesktopCertificateServiceError("busy");
|
|
64
|
+
if (desktopCompatibilityRuntimeActive()) throw new DesktopCertificateServiceError("runtime_running");
|
|
65
|
+
let release: () => void;
|
|
66
|
+
try { release = acquireDesktopCertificateMutation(); } catch { throw new DesktopCertificateServiceError("busy"); }
|
|
67
|
+
busy = kind;
|
|
68
|
+
try {
|
|
69
|
+
if (kind !== "prepare") {
|
|
70
|
+
const before = inspect();
|
|
71
|
+
if (!("fingerprint" in before)) throw new DesktopCertificateServiceError("not_prepared");
|
|
72
|
+
if (before.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
|
|
73
|
+
if (kind === "remove-trust" || kind === "renew") {
|
|
74
|
+
const running = await appRunning();
|
|
75
|
+
if (running === null) throw new DesktopCertificateServiceError("app_state_unknown");
|
|
76
|
+
if (running) throw new DesktopCertificateServiceError("app_running");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const authority = kind === "prepare" ? await prepare() : await load(kind !== "trust");
|
|
80
|
+
if (kind !== "prepare") {
|
|
81
|
+
if (authority.fingerprint !== expectedFingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
|
|
82
|
+
const current = inspect();
|
|
83
|
+
if (!("fingerprint" in current) || current.fingerprint !== authority.fingerprint) throw new DesktopCertificateServiceError("fingerprint_changed");
|
|
84
|
+
const result = await changeTrust(authority, kind === "trust" ? "trust" : "remove");
|
|
85
|
+
if (result === "unknown") throw new DesktopCertificateServiceError("trust_unknown");
|
|
86
|
+
if (result !== (kind === "trust" ? "trusted" : "not-trusted")) throw new DesktopCertificateServiceError("trust_not_applied");
|
|
87
|
+
// Never discard the only removable old identity until OS trust removal is proven.
|
|
88
|
+
// Publication failure leaves its protected envelope intact for an explicit retry.
|
|
89
|
+
if (kind === "renew") await renew(authority.fingerprint);
|
|
90
|
+
}
|
|
91
|
+
busy = null;
|
|
92
|
+
return status();
|
|
93
|
+
} finally { busy = null; release(); }
|
|
94
|
+
}
|
|
95
|
+
return { status, prepare: () => action("prepare"), trust: (fingerprint: string) => action("trust", fingerprint),
|
|
96
|
+
removeTrust: (fingerprint: string) => action("remove-trust", fingerprint), renew: (fingerprint: string) => action("renew", fingerprint) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type DesktopCertificateService = ReturnType<typeof createDesktopCertificateService>;
|