@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { collapseAntigravityPublicModels } from "../../providers/antigravity-effort-families";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
5
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
@@ -504,6 +505,7 @@ export function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<Catalo
|
|
|
504
505
|
}
|
|
505
506
|
|
|
506
507
|
export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
508
|
+
goModels = collapseAntigravityPublicModels(goModels);
|
|
507
509
|
const collisionSkipped = resolveSlugAliasCollisions(goModels);
|
|
508
510
|
const comboPublicSlugs = new Set(goModels
|
|
509
511
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
@@ -525,6 +527,7 @@ export function uniqueCatalogModelsForPublicList(goModels: CatalogModel[]): Cata
|
|
|
525
527
|
}
|
|
526
528
|
|
|
527
529
|
export function uniqueCatalogModelsForRawPublicList(goModels: CatalogModel[]): CatalogModel[] {
|
|
530
|
+
goModels = collapseAntigravityPublicModels(goModels);
|
|
528
531
|
const publicId = (model: CatalogModel): string => model.alias ?? `${model.provider}/${model.id}`;
|
|
529
532
|
const comboPublicIds = new Set(goModels
|
|
530
533
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
@@ -372,7 +372,7 @@ export function orderForModelPicker(
|
|
|
372
372
|
* user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
|
|
373
373
|
* describes history compatibility; neither is an authorship signal.
|
|
374
374
|
*/
|
|
375
|
-
function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
375
|
+
export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
376
376
|
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
377
377
|
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
378
378
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
@@ -29,6 +29,7 @@ import type { NormalizedComboConfig } from "../../combos/types";
|
|
|
29
29
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
30
30
|
import { redactSecretString } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
|
+
import { decodeOverlayState } from "../shim-state-file";
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
|
|
@@ -196,14 +197,20 @@ export function codexCommandCandidates(): string[] {
|
|
|
196
197
|
|
|
197
198
|
export function codexShimCommandCandidates(): string[] {
|
|
198
199
|
try {
|
|
199
|
-
const
|
|
200
|
+
const configDir = getConfigDir();
|
|
201
|
+
const state = JSON.parse(readFileSync(join(configDir, "codex-shim.json"), "utf8")) as {
|
|
202
|
+
schema?: unknown;
|
|
203
|
+
mode?: unknown;
|
|
200
204
|
wrapperPath?: unknown;
|
|
201
205
|
originalPath?: unknown;
|
|
202
206
|
backupPath?: unknown;
|
|
203
207
|
wrappers?: Array<{ wrapperPath?: unknown; originalPath?: unknown; backupPath?: unknown }>;
|
|
204
208
|
};
|
|
209
|
+
const overlay = decodeOverlayState(state, configDir);
|
|
210
|
+
if ((state.schema !== undefined || state.mode !== undefined) && !overlay) return [];
|
|
205
211
|
const files = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
206
|
-
const out: string[] =
|
|
212
|
+
const out: string[] = overlay && isSpawnableCodexCandidate(overlay.launcherPath)
|
|
213
|
+
? [overlay.launcherPath] : [];
|
|
207
214
|
for (const file of files) {
|
|
208
215
|
for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
|
|
209
216
|
if (typeof value !== "string" || value.length === 0) continue;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, type Stats } from "node:fs";
|
|
2
|
+
import { readCodexHomeJournal } from "../codex-home-owner";
|
|
3
|
+
import { samePath, type RawCatalog } from "./parsing";
|
|
4
|
+
|
|
5
|
+
export const CATALOG_HEAL_MAX_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export interface CatalogObservation {
|
|
8
|
+
readonly signature: string;
|
|
9
|
+
readonly catalog: RawCatalog | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Canonical path equality when available; unavailable paths retain lexical equality. */
|
|
13
|
+
export function sameCatalogHealPath(left: string, right: string): boolean {
|
|
14
|
+
try { return samePath(realpathSync.native(left), realpathSync.native(right)); }
|
|
15
|
+
catch { return samePath(left, right); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve authority without reading catalog bytes or cleaning journal evidence. */
|
|
19
|
+
export function selectCatalogHealPath(
|
|
20
|
+
journalPath: string,
|
|
21
|
+
defaultPath: string,
|
|
22
|
+
resolvePath: (path: string) => string,
|
|
23
|
+
): string | null {
|
|
24
|
+
const inspected = readCodexHomeJournal(journalPath);
|
|
25
|
+
if (inspected.kind === "unknown") return null;
|
|
26
|
+
if (inspected.kind === "missing") return defaultPath;
|
|
27
|
+
const selected = inspected.journal.injectedCatalogPath;
|
|
28
|
+
if (selected === undefined || selected === null) return defaultPath;
|
|
29
|
+
// A selected custom catalog remains selected even when missing or corrupt.
|
|
30
|
+
if (typeof selected !== "string" || !selected.trim() || selected.includes("\0")) return null;
|
|
31
|
+
try { return resolvePath(selected); }
|
|
32
|
+
catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function signature(stat: Stats): string {
|
|
36
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Bounded regular descriptor reads; never open a FIFO/device or follow a replaced symlink. */
|
|
40
|
+
export function observeCatalogHealFile(path: string, readContent: boolean): CatalogObservation | null {
|
|
41
|
+
let fd: number | undefined;
|
|
42
|
+
try {
|
|
43
|
+
const before = lstatSync(path);
|
|
44
|
+
if (!before.isFile() || before.size > CATALOG_HEAL_MAX_BYTES) return null;
|
|
45
|
+
if (!readContent) return { signature: signature(before), catalog: null };
|
|
46
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
47
|
+
const opened = fstatSync(fd);
|
|
48
|
+
if (!opened.isFile() || opened.size > CATALOG_HEAL_MAX_BYTES || signature(before) !== signature(opened)) return null;
|
|
49
|
+
const bytes = Buffer.alloc(opened.size + 1);
|
|
50
|
+
let length = 0;
|
|
51
|
+
while (length < bytes.length) {
|
|
52
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
53
|
+
if (count === 0) break;
|
|
54
|
+
length += count;
|
|
55
|
+
}
|
|
56
|
+
if (length !== opened.size || signature(fstatSync(fd)) !== signature(opened)
|
|
57
|
+
|| signature(lstatSync(path)) !== signature(opened)) return null;
|
|
58
|
+
let catalog: RawCatalog | null = null;
|
|
59
|
+
try {
|
|
60
|
+
const value: unknown = JSON.parse(bytes.subarray(0, length).toString("utf8"));
|
|
61
|
+
if (value && typeof value === "object" && Array.isArray((value as RawCatalog).models)) catalog = value as RawCatalog;
|
|
62
|
+
} catch { /* Invalid catalog bytes remain unavailable until their stat changes. */ }
|
|
63
|
+
return { signature: signature(opened), catalog };
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
66
|
+
}
|
|
@@ -295,7 +295,8 @@ export function applyProviderConfigHints(
|
|
|
295
295
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
296
296
|
}
|
|
297
297
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
298
|
-
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
|
|
298
|
+
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id)
|
|
299
|
+
?? (model.antigravityEffortWireModelIds !== undefined);
|
|
299
300
|
const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
|
|
300
301
|
const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
|
|
301
302
|
const supportsVerbosity = staticPolicy.model.supportsVerbosity;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
2
|
+
import { antigravityFamilyDisabled, collapseAntigravityPublicModels, projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
|
|
2
3
|
import { XAI_OAUTH_FAST_VARIANT_IDS } from "../../providers/xai-fast-model";
|
|
3
4
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
4
5
|
import { execFileSync } from "node:child_process";
|
|
@@ -267,7 +268,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
|
|
|
267
268
|
|
|
268
269
|
export function filterCatalogVisibleModels(
|
|
269
270
|
models: CatalogModel[],
|
|
270
|
-
config: Pick<OcxConfig, "disabledModels" | "providers">,
|
|
271
|
+
config: Pick<OcxConfig, "disabledModels" | "providers" | "modelDiscovery">,
|
|
271
272
|
): CatalogModel[] {
|
|
272
273
|
const disabled = new Set(config.disabledModels ?? []);
|
|
273
274
|
const allowByProvider = new Map<string, Set<string>>();
|
|
@@ -289,12 +290,14 @@ export function filterCatalogVisibleModels(
|
|
|
289
290
|
// Two catalog stages with different equivalence relations is the exact bug class
|
|
290
291
|
// this change exists to remove.
|
|
291
292
|
if (Array.isArray(sel) && sel.length > 0) {
|
|
292
|
-
allowByProvider.set(name, new Set(
|
|
293
|
+
allowByProvider.set(name, new Set(projectAntigravitySelectedModels(name, sel, models)
|
|
294
|
+
.map(model => slugEquivalenceKey(routedSlug(name, model)))));
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
|
-
return models.filter(m => {
|
|
297
|
+
return collapseAntigravityPublicModels(models).filter(m => {
|
|
296
298
|
if (initialModelSelectionPending(config.providers[m.provider])) return false;
|
|
297
299
|
if (config.providers[m.provider]?.disabled === true) return false;
|
|
300
|
+
if (antigravityFamilyDisabled(config, m, models)) return false;
|
|
298
301
|
const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
|
|
299
302
|
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
|
|
300
303
|
for (const stored of disabled) {
|
|
@@ -113,6 +113,8 @@ export interface CatalogModel {
|
|
|
113
113
|
displayName?: string;
|
|
114
114
|
owned_by?: string;
|
|
115
115
|
reasoningEfforts?: string[];
|
|
116
|
+
/** Exact CCA family evidence, retained through the in-memory discovery cache. */
|
|
117
|
+
antigravityEffortWireModelIds?: Partial<Record<"low" | "medium" | "high", string>>;
|
|
116
118
|
/** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
|
|
117
119
|
suppressSyntheticMax?: boolean;
|
|
118
120
|
defaultReasoningEffort?: string;
|
|
@@ -751,20 +751,29 @@ export async function fetchProviderModelsWithAuth(
|
|
|
751
751
|
const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
|
|
752
752
|
id: model.id,
|
|
753
753
|
provider: name,
|
|
754
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
// Only an exact discovered wire map proves a new model's selectable effort ladder.
|
|
755
|
+
reasoningEfforts: Object.keys(model.effortWireModelIds ?? {}),
|
|
756
|
+
...(model.effortWireModelIds ? {
|
|
757
|
+
antigravityEffortWireModelIds: model.effortWireModelIds,
|
|
758
|
+
suppressSyntheticMax: true,
|
|
759
|
+
defaultReasoningEffort: model.effortWireModelIds.medium ? "medium" : "high",
|
|
760
|
+
} : {}),
|
|
757
761
|
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
758
762
|
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
759
763
|
}, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
|
|
760
764
|
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
761
|
-
if (!
|
|
765
|
+
if (!isCurrentCacheGeneration()) {
|
|
762
766
|
return observed(withConfiguredRetention(configured), "degraded");
|
|
763
767
|
}
|
|
768
|
+
// Save exact family routes before publishing synthetic IDs. These synchronous writes
|
|
769
|
+
// cannot interleave with a credential-generation change; failure keeps the prior cache.
|
|
764
770
|
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
|
|
765
771
|
provider: name,
|
|
766
772
|
cacheGeneration,
|
|
767
773
|
});
|
|
774
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
775
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
776
|
+
}
|
|
768
777
|
markProviderDiscoveryOk(name, live.length);
|
|
769
778
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
770
779
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CatalogWriteIntent } from "./write-audit-contract";
|
|
2
|
+
|
|
3
|
+
export type CatalogPublicationEvent =
|
|
4
|
+
| { readonly kind: "published"; readonly path: string; readonly intent: CatalogWriteIntent }
|
|
5
|
+
| { readonly kind: "native-released"; readonly path: string | null };
|
|
6
|
+
|
|
7
|
+
const listeners = new Set<(event: CatalogPublicationEvent) => void>();
|
|
8
|
+
|
|
9
|
+
/** Process-local notifications; subscribers never own publication success. */
|
|
10
|
+
export function subscribeCatalogPublication(listener: (event: CatalogPublicationEvent) => void): () => void {
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => { listeners.delete(listener); };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function notifyCatalogPublication(event: CatalogPublicationEvent): void {
|
|
16
|
+
for (const listener of [...listeners]) {
|
|
17
|
+
try { listener(event); } catch { /* A subscriber cannot fail or undo publication. */ }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -7,7 +7,7 @@ import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
|
|
|
7
7
|
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
8
8
|
import { getCodexHome } from "../paths";
|
|
9
9
|
import { readCodexCatalogPathForHome } from "./parsing";
|
|
10
|
-
import {
|
|
10
|
+
import { invalidateCodexModelsCacheWithPermitOutcome } from "./sync";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
13
|
const MAX_MODELS = 2_000;
|
|
@@ -17,7 +17,7 @@ const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
|
|
|
17
17
|
export type RemoteCatalogFailureCode =
|
|
18
18
|
| "url_invalid" | "insecure_http_refused" | "credential_invalid" | "request_failed"
|
|
19
19
|
| "redirect_refused" | "http_error" | "body_too_large" | "body_invalid"
|
|
20
|
-
| "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path";
|
|
20
|
+
| "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path" | "foreign_owner" | "owner_unknown";
|
|
21
21
|
|
|
22
22
|
export class RemoteCatalogError extends Error {
|
|
23
23
|
constructor(readonly code: RemoteCatalogFailureCode, message: string, readonly status?: number) {
|
|
@@ -202,7 +202,10 @@ export async function fetchRemoteCatalog(
|
|
|
202
202
|
|
|
203
203
|
function mapSerializationFailure<T>(outcome: CatalogSerializationOutcome<T>): never {
|
|
204
204
|
if (outcome.kind === "completed") throw new RemoteCatalogError("write_failed", "Remote catalog installation failed");
|
|
205
|
-
const code = outcome.reason === "busy" ? "lock_busy"
|
|
205
|
+
const code = outcome.reason === "busy" ? "lock_busy"
|
|
206
|
+
: outcome.reason === "database" ? "lock_database"
|
|
207
|
+
: outcome.reason === "foreign-owner" ? "foreign_owner"
|
|
208
|
+
: outcome.reason === "owner-unknown" ? "owner_unknown" : "unsafe_path";
|
|
206
209
|
throw new RemoteCatalogError(code, `Remote catalog installation unavailable (${outcome.reason})`);
|
|
207
210
|
}
|
|
208
211
|
|
|
@@ -235,23 +238,18 @@ export async function pullRemoteCatalog(input: string, options: PullRemoteCatalo
|
|
|
235
238
|
const fetched = await fetchRemoteCatalog(input, options);
|
|
236
239
|
const codexHome = options.codexHome ?? getCodexHome();
|
|
237
240
|
const catalogPath = readCodexCatalogPathForHome(codexHome);
|
|
238
|
-
const current = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
239
|
-
const candidate = Buffer.from(fetched.content, "utf8");
|
|
240
|
-
if (current?.equals(candidate)) {
|
|
241
|
-
return { status: "unchanged", catalogWritten: false, cacheSynced: false, codexHome, catalogPath, modelCount: fetched.document.models.length };
|
|
242
|
-
}
|
|
243
241
|
const outcome = withCatalogWriteSerialization(codexHome, permit => {
|
|
244
242
|
// Re-check under K: another writer may have installed these bytes while the request was in flight.
|
|
245
243
|
const lockedCurrent = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
|
|
244
|
+
const catalog = replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });
|
|
245
|
+
if (catalog.kind === "refused") throw new RemoteCatalogError("write_failed", "Remote catalog publication refused");
|
|
246
|
+
const cache = invalidateCodexModelsCacheWithPermitOutcome(permit, codexHome, { allowWhenDesiredDisabled: true });
|
|
247
|
+
if (cache !== "written" && cache !== "unchanged") {
|
|
248
|
+
if (catalog.kind === "written") restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
|
|
251
249
|
throw new RemoteCatalogError("write_failed", "Remote catalog cache synchronization failed");
|
|
252
250
|
}
|
|
253
|
-
return { catalogWritten:
|
|
254
|
-
});
|
|
251
|
+
return { catalogWritten: catalog.kind === "written", cacheSynced: cache === "written" };
|
|
252
|
+
}, { intent: "pull", writer: "catalog-pull" });
|
|
255
253
|
if (outcome.kind !== "completed") return mapSerializationFailure(outcome);
|
|
256
254
|
return {
|
|
257
255
|
status: outcome.value.catalogWritten ? "updated" : "unchanged",
|
|
@@ -6,9 +6,12 @@ import { RETIRED_NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./m
|
|
|
6
6
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
7
7
|
import {
|
|
8
8
|
withCatalogWriteSerialization,
|
|
9
|
+
CatalogWritePermitRefusal,
|
|
9
10
|
type CatalogWritePermit,
|
|
10
11
|
} from "../catalog-write-serialization";
|
|
11
12
|
import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
|
|
13
|
+
import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE } from "./routed-removal";
|
|
14
|
+
import { notifyCatalogPublication } from "./publication-observer";
|
|
12
15
|
|
|
13
16
|
function visibleAccountReplacementNatives(
|
|
14
17
|
models: readonly RawEntry[],
|
|
@@ -72,7 +75,10 @@ export function restoreCodexCatalogWithPermit(
|
|
|
72
75
|
): { removed: number; kept: number; path: string } {
|
|
73
76
|
const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
|
|
74
77
|
const catalog = readCatalog(catalogPath);
|
|
75
|
-
if (!catalog || !Array.isArray(catalog.models))
|
|
78
|
+
if (!catalog || !Array.isArray(catalog.models)) {
|
|
79
|
+
notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
|
|
80
|
+
return { removed: 0, kept: 0, path: catalogPath };
|
|
81
|
+
}
|
|
76
82
|
const disabledModels = currentDisabledModelsForRestore();
|
|
77
83
|
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
78
84
|
const backup = readCatalogBackup(catalogPath);
|
|
@@ -94,10 +100,11 @@ export function restoreCodexCatalogWithPermit(
|
|
|
94
100
|
models: [...backup.models.filter(m => typeof m.slug !== "string"
|
|
95
101
|
|| !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
|
|
96
102
|
};
|
|
97
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
103
|
+
const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
98
104
|
path: catalogPath,
|
|
99
105
|
content: `${JSON.stringify(restored, null, 2)}\n`,
|
|
100
106
|
});
|
|
107
|
+
if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
|
|
101
108
|
return { removed, kept: restored.models.length, path: catalogPath };
|
|
102
109
|
}
|
|
103
110
|
const before = catalog.models.length;
|
|
@@ -110,11 +117,13 @@ export function restoreCodexCatalogWithPermit(
|
|
|
110
117
|
const removed = before - native.length;
|
|
111
118
|
if (removed > 0) {
|
|
112
119
|
catalog.models = native;
|
|
113
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
120
|
+
const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
114
121
|
path: catalogPath,
|
|
115
122
|
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
116
123
|
});
|
|
124
|
+
if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
|
|
117
125
|
}
|
|
126
|
+
if (removed === 0) notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
|
|
118
127
|
return { removed, kept: native.length, path: catalogPath };
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -123,10 +132,14 @@ export function restoreCodexCatalog(): { removed: number; kept: number; path: st
|
|
|
123
132
|
const outcome = withCatalogWriteSerialization(
|
|
124
133
|
owningCodexHome,
|
|
125
134
|
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
135
|
+
{ intent: "restore", writer: "catalog-restore" },
|
|
126
136
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
if (outcome.kind === "unavailable") {
|
|
138
|
+
throw new CatalogWritePermitRefusal(outcome.reason === "foreign-owner"
|
|
139
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE : outcome.reason === "owner-unknown"
|
|
140
|
+
? UNKNOWN_CODEX_HOME_OWNER_MESSAGE : `Catalog restoration unavailable (${outcome.reason}).`);
|
|
141
|
+
}
|
|
142
|
+
return outcome.value;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { projectAntigravitySelectedModels } from "../../providers/antigravity-effort-families";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { initializeConfigOwnership } from "../../lib/config-ownership";
|
|
4
|
-
import { getConfigDir, loadConfig, websocketsEnabled } from "../../config";
|
|
5
|
+
import { getConfigDir, loadConfig, websocketsEnabled, withConfigMutationLockSync } from "../../config";
|
|
5
6
|
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
6
7
|
import { legacyCustomModelCatalogSlugs } from "../custom-model-catalog-migration";
|
|
7
8
|
import { getCodexHome } from "../paths";
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
nativeMultiAgentDefaults,
|
|
40
41
|
} from "./parsing";
|
|
41
42
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
43
|
+
import { ocxRoutedNamespaceCounts, routedRemovalBackedByConfigFile, unconfiguredRoutedRemoval } from "./routed-removal";
|
|
44
|
+
import { ConfigMutationLockError } from "../../config/mutation-lock";
|
|
42
45
|
import {
|
|
43
46
|
accountBoundNativeOpenAiSlugsBySelector,
|
|
44
47
|
desktopAllowlistSuppressedNativeSlugs,
|
|
@@ -63,11 +66,13 @@ import {
|
|
|
63
66
|
} from "../catalog-write-serialization";
|
|
64
67
|
import {
|
|
65
68
|
preparedBytesDifferFromDisk,
|
|
69
|
+
auditRefusedCatalogReplacement,
|
|
66
70
|
publishHashedCodexCatalogBackup,
|
|
67
71
|
publishLegacyCodexCatalogBackup,
|
|
68
72
|
replaceActiveCodexCatalog,
|
|
69
73
|
replaceCodexModelsCache,
|
|
70
74
|
type PreparedCatalogFileWrite,
|
|
75
|
+
type CatalogFileReplacement,
|
|
71
76
|
} from "../internal/catalog-writer";
|
|
72
77
|
import { visibleCodexAccountSelectors } from "./account-models";
|
|
73
78
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
@@ -104,8 +109,15 @@ interface RetainedCatalogSyncResult {
|
|
|
104
109
|
comboOmissions: ComboCatalogOmission[];
|
|
105
110
|
/** Validated catalog commit (including identical bytes), or a refused refresh. */
|
|
106
111
|
refreshOutcome?: "committed" | "refused";
|
|
107
|
-
/**
|
|
108
|
-
|
|
112
|
+
/**
|
|
113
|
+
* `desired_disabled`: observed under K after the provider await. `unbacked_routed_removal`: the
|
|
114
|
+
* catalog would lose routed namespaces config.json still enables, or config.json is missing or
|
|
115
|
+
* unreadable (#6529). `foreign_owner`: this Codex home is bound to another OPENCODEX_HOME. Nothing
|
|
116
|
+
* was written in any of these cases.
|
|
117
|
+
*/
|
|
118
|
+
skippedReason?: "desired_disabled" | "unbacked_routed_removal" | "foreign_owner" | "owner_unknown";
|
|
119
|
+
/** With `unbacked_routed_removal`: how many routed namespaces the refused write would have emptied. */
|
|
120
|
+
protectedRoutedNamespaces?: number;
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
/**
|
|
@@ -465,7 +477,7 @@ function writeRetainedCatalogSync({
|
|
|
465
477
|
provider.disabled !== true
|
|
466
478
|
&& Array.isArray(provider.selectedModels)
|
|
467
479
|
&& provider.selectedModels.length > 0
|
|
468
|
-
? [[name, new Set(provider.selectedModels)] as const]
|
|
480
|
+
? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, goModels))] as const]
|
|
469
481
|
: []
|
|
470
482
|
)),
|
|
471
483
|
);
|
|
@@ -553,12 +565,46 @@ function writeRetainedCatalogSync({
|
|
|
553
565
|
if (!preparedBytesDifferFromDisk(preparedCatalog)) {
|
|
554
566
|
return { added, path: catalogPath, catalogWritten: false, comboOmissions };
|
|
555
567
|
}
|
|
556
|
-
|
|
557
|
-
|
|
568
|
+
// A refresh may drop a provider's rows only when config.json on disk agrees the provider is gone.
|
|
569
|
+
// Without that, a config that is not the user's (missing or unreadable file read as defaults,
|
|
570
|
+
// another OPENCODEX_HOME) would publish a native-only catalog and exit cleanly (#6529).
|
|
571
|
+
const refused = (protectedRoutedNamespaces: number): RetainedCatalogSyncResult => ({
|
|
572
|
+
added: 0,
|
|
573
|
+
path: catalogPath,
|
|
574
|
+
catalogWritten: false,
|
|
575
|
+
comboOmissions,
|
|
576
|
+
skippedReason: "unbacked_routed_removal",
|
|
577
|
+
protectedRoutedNamespaces,
|
|
578
|
+
});
|
|
579
|
+
let replacement: CatalogFileReplacement | undefined;
|
|
580
|
+
const removal = unconfiguredRoutedRemoval(onDiskCatalog, catalog, config);
|
|
581
|
+
if (removal !== null) {
|
|
582
|
+
// Hold C (K -> C) from the config read through the replacement, so a save that enables one of
|
|
583
|
+
// these providers cannot land between the check and the write. A held C is a refusal too.
|
|
584
|
+
let backed = false;
|
|
585
|
+
try {
|
|
586
|
+
backed = withConfigMutationLockSync(() => {
|
|
587
|
+
if (!routedRemovalBackedByConfigFile(removal)) return false;
|
|
588
|
+
replacement = replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
|
|
589
|
+
return replacement.kind !== "refused";
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (!(error instanceof ConfigMutationLockError)) throw error;
|
|
593
|
+
}
|
|
594
|
+
if (!backed) {
|
|
595
|
+
if (replacement?.kind !== "refused") {
|
|
596
|
+
auditRefusedCatalogReplacement(permit, owningCodexHome, preparedCatalog, "unbacked-routed-removal");
|
|
597
|
+
}
|
|
598
|
+
return refused(removal.namespaces.length);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
replacement = replaceActiveCodexCatalog(permit, owningCodexHome, preparedCatalog);
|
|
602
|
+
if (replacement.kind === "refused") return refused(ocxRoutedNamespaceCounts(onDiskCatalog).size);
|
|
603
|
+
}
|
|
558
604
|
return {
|
|
559
605
|
added,
|
|
560
606
|
path: catalogPath,
|
|
561
|
-
catalogWritten:
|
|
607
|
+
catalogWritten: replacement?.kind === "written",
|
|
562
608
|
comboOmissions,
|
|
563
609
|
};
|
|
564
610
|
}
|
|
@@ -648,7 +694,7 @@ export async function syncCatalogModels(
|
|
|
648
694
|
owningCodexHome,
|
|
649
695
|
modelEntitlements,
|
|
650
696
|
});
|
|
651
|
-
});
|
|
697
|
+
}, { intent: "refresh", writer: "retained-sync" });
|
|
652
698
|
if (committed.kind === "completed" && committed.value !== null) {
|
|
653
699
|
return {
|
|
654
700
|
...committed.value,
|
|
@@ -661,6 +707,10 @@ export async function syncCatalogModels(
|
|
|
661
707
|
catalogWritten: false,
|
|
662
708
|
comboOmissions,
|
|
663
709
|
refreshOutcome: "refused",
|
|
710
|
+
...(committed.kind === "unavailable" && committed.reason === "foreign-owner"
|
|
711
|
+
? { skippedReason: "foreign_owner" as const } : {}),
|
|
712
|
+
...(committed.kind === "unavailable" && committed.reason === "owner-unknown"
|
|
713
|
+
? { skippedReason: "owner_unknown" as const } : {}),
|
|
664
714
|
};
|
|
665
715
|
}
|
|
666
716
|
|
|
@@ -728,9 +778,7 @@ export function invalidateCodexModelsCacheWithPermitOutcome(
|
|
|
728
778
|
// `cacheSynced` mean what its name and its consumers already assume, and what
|
|
729
779
|
// `pullRemoteCatalog` and the early returns in `refreshCodexModelCatalog`
|
|
730
780
|
// already assert: a write happened.
|
|
731
|
-
|
|
732
|
-
replaceCodexModelsCache(permit, owningCodexHome, preparedCache);
|
|
733
|
-
return "written";
|
|
781
|
+
return replaceCodexModelsCache(permit, owningCodexHome, preparedCache).kind;
|
|
734
782
|
} catch {
|
|
735
783
|
return "failed";
|
|
736
784
|
}
|
|
@@ -749,6 +797,7 @@ export function invalidateCodexModelsCache(options?: CodexCatalogSyncOptions): b
|
|
|
749
797
|
const outcome = withCatalogWriteSerialization(
|
|
750
798
|
owningCodexHome,
|
|
751
799
|
permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, options),
|
|
800
|
+
{ intent: "cache", writer: "cache-invalidate" },
|
|
752
801
|
);
|
|
753
802
|
return outcome.kind === "completed" && outcome.value;
|
|
754
803
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { COMBO_NAMESPACE } from "../../combos";
|
|
2
|
+
import { readConfigAdmissionSnapshot, type ConfigAdmissionSnapshot } from "../../config/diagnostics";
|
|
3
|
+
import type { OcxConfig } from "../../types";
|
|
4
|
+
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
5
|
+
import { isOcxAuthoredRoutedEntry } from "./build-entries";
|
|
6
|
+
import { isNativeAliasCatalogEntry } from "./metadata";
|
|
7
|
+
import type { RawCatalog, RawEntry } from "./parsing";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Routed namespaces a candidate catalog empties of OpenCodex-authored rows only because the config
|
|
11
|
+
* that produced it does not enable them: the "deleted provider" rule in build-entries. That rule is
|
|
12
|
+
* the one way a refresh removes a provider's rows wholesale, so it is the one a config that is not
|
|
13
|
+
* the user's (a missing or unreadable config.json read as defaults, another OPENCODEX_HOME) turns
|
|
14
|
+
* into a silent native-only catalog (#6529).
|
|
15
|
+
*/
|
|
16
|
+
export interface UnconfiguredRoutedRemoval {
|
|
17
|
+
readonly namespaces: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RoutedNamespaceConfig = Pick<OcxConfig, "providers" | "combos">;
|
|
21
|
+
|
|
22
|
+
function routedNamespace(entry: RawEntry): string | null {
|
|
23
|
+
if (!entry || typeof entry !== "object" || typeof entry.slug !== "string") return null;
|
|
24
|
+
// Account-bound native rows follow the account pool, not provider config.
|
|
25
|
+
if (trustedAccountBoundNativeCatalogSlug(entry) !== undefined) return null;
|
|
26
|
+
if (isNativeAliasCatalogEntry(entry)) return COMBO_NAMESPACE;
|
|
27
|
+
// Combo aliases may be bare or use an unrelated provider namespace. Ownership selects the
|
|
28
|
+
// combo namespace only with the same generated-description authorship signal as routed rows.
|
|
29
|
+
if (entry.owned_by === COMBO_NAMESPACE
|
|
30
|
+
&& typeof entry.description === "string"
|
|
31
|
+
&& entry.description.startsWith("Routed via opencodex → ")) return COMBO_NAMESPACE;
|
|
32
|
+
// Foreign rows (Cursor, user tooling) are never removed by the provider rule.
|
|
33
|
+
if (!isOcxAuthoredRoutedEntry(entry)) return null;
|
|
34
|
+
return entry.slug.slice(0, entry.slug.indexOf("/"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** OpenCodex-authored routed rows per namespace; foreign and account-bound rows never count. */
|
|
38
|
+
export function ocxRoutedNamespaceCounts(catalog: RawCatalog | null): Map<string, number> {
|
|
39
|
+
const counts = new Map<string, number>();
|
|
40
|
+
for (const entry of catalog?.models ?? []) {
|
|
41
|
+
const namespace = routedNamespace(entry);
|
|
42
|
+
if (namespace !== null) counts.set(namespace, (counts.get(namespace) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
return counts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Total OpenCodex-authored routed rows, or null for a value that is not a catalog. */
|
|
48
|
+
export function ocxRoutedRowCount(catalog: unknown): number | null {
|
|
49
|
+
if (catalog === null || typeof catalog !== "object" || !Array.isArray((catalog as RawCatalog).models)) return null;
|
|
50
|
+
let total = 0;
|
|
51
|
+
for (const count of ocxRoutedNamespaceCounts(catalog as RawCatalog).values()) total += count;
|
|
52
|
+
return total;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether a config still produces routed rows for a namespace: an enabled provider, or any combo. */
|
|
56
|
+
export function configEnablesRoutedNamespace(config: RoutedNamespaceConfig, namespace: string): boolean {
|
|
57
|
+
if (namespace === COMBO_NAMESPACE && Object.keys(config.combos ?? {}).length > 0) return true;
|
|
58
|
+
const provider = config.providers?.[namespace];
|
|
59
|
+
return provider !== undefined && provider.disabled !== true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The namespaces whose OpenCodex-authored routed rows are on disk now, absent from the candidate,
|
|
64
|
+
* and not enabled by the driving config; null when the candidate removes no such namespace.
|
|
65
|
+
* Removals inside a namespace the driving config still enables (discovery, selection, a deleted
|
|
66
|
+
* custom model) are the provider's own business and are not reported here.
|
|
67
|
+
*/
|
|
68
|
+
export function unconfiguredRoutedRemoval(
|
|
69
|
+
active: RawCatalog | null,
|
|
70
|
+
candidate: RawCatalog,
|
|
71
|
+
driving: RoutedNamespaceConfig,
|
|
72
|
+
): UnconfiguredRoutedRemoval | null {
|
|
73
|
+
const activeModels = active?.models ?? [];
|
|
74
|
+
if (activeModels.length === 0) return null;
|
|
75
|
+
const kept = new Set((candidate.models ?? []).flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []));
|
|
76
|
+
const namespaces = new Set<string>();
|
|
77
|
+
for (const entry of activeModels) {
|
|
78
|
+
if (typeof entry.slug === "string" && kept.has(entry.slug)) continue;
|
|
79
|
+
const namespace = routedNamespace(entry);
|
|
80
|
+
if (namespace !== null && !configEnablesRoutedNamespace(driving, namespace)) namespaces.add(namespace);
|
|
81
|
+
}
|
|
82
|
+
return namespaces.size > 0 ? { namespaces: [...namespaces].sort() } : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when config.json on disk backs every removal: it is a readable, parseable file, and it enables
|
|
87
|
+
* none of the removed namespaces. A missing, unreadable or salvaged file backs nothing, and a file
|
|
88
|
+
* that still enables a namespace shows the driving config was not the one on disk. Read once, at
|
|
89
|
+
* write time, so a config that fell back to defaults while the file was briefly unreadable cannot
|
|
90
|
+
* pass on a later successful read.
|
|
91
|
+
*/
|
|
92
|
+
export function routedRemovalBackedByConfigFile(
|
|
93
|
+
removal: UnconfiguredRoutedRemoval,
|
|
94
|
+
read: () => ConfigAdmissionSnapshot = readConfigAdmissionSnapshot,
|
|
95
|
+
): boolean {
|
|
96
|
+
const snapshot = read();
|
|
97
|
+
if (snapshot.kind !== "read" || snapshot.diagnostics.source !== "file") return false;
|
|
98
|
+
return removal.namespaces.every(namespace => !configEnablesRoutedNamespace(snapshot.diagnostics.config, namespace));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One line for logs when K refused a writer from another OPENCODEX_HOME; names no path. */
|
|
102
|
+
export const FOREIGN_CODEX_HOME_OWNER_MESSAGE = "Codex catalog left unchanged: this Codex home was set up by "
|
|
103
|
+
+ "another OpenCodex home (OPENCODEX_HOME), recorded in its opencodex-journal.json. Run opencodex "
|
|
104
|
+
+ "from that home, or run `ocx restore` there first.";
|
|
105
|
+
|
|
106
|
+
/** Unavailable evidence is not proof of a foreign owner. No paths or journal content are exposed. */
|
|
107
|
+
export const UNKNOWN_CODEX_HOME_OWNER_MESSAGE = "Codex catalog left unchanged: ownership of this Codex home "
|
|
108
|
+
+ "could not be checked. Restore readable ownership evidence and retry.";
|
|
109
|
+
|
|
110
|
+
/** One line for logs: counts only, never provider names, model ids or paths. */
|
|
111
|
+
export function unbackedRoutedRemovalMessage(count: number): string {
|
|
112
|
+
return `Codex catalog left unchanged: this refresh would remove the routed models of ${count} `
|
|
113
|
+
+ `provider namespace${count === 1 ? "" : "s"} that config.json still enables, or config.json `
|
|
114
|
+
+ "is missing or unreadable. Run `ocx sync` again once config.json is readable; use `ocx restore` "
|
|
115
|
+
+ "to remove routed models on purpose.";
|
|
116
|
+
}
|