@bitkyc08/opencodex 2.83.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-DyR9yBee.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +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 +2 -2
- package/package.json +2 -3
- 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/protocol.ts +15 -8
- 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/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +56 -15
- 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 +24 -0
- package/src/bridge.ts +1 -1
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/context-windows.ts +54 -1
- 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/connect-proxy.ts +10 -3
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- 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/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +14 -2
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +60 -11
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +21 -4
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +15 -2
- package/src/cli/help.ts +2 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +26 -8
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +20 -7
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- 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 +59 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +7 -1
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +6 -3
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-models.ts +16 -2
- 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/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +15 -6
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +18 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +56 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +8 -2
- package/src/config/paths.ts +13 -3
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +19 -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 +713 -285
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- 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 +14 -7
- package/src/integrations/registry.ts +52 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/store.ts +3 -0
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +24 -7
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -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/json-byte-size.ts +31 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- 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/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +28 -17
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +55 -6
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- 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/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +21 -20
- package/src/providers/registry/entries-extended.ts +19 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +37 -20
- 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 +46 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +15 -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/lifecycle.ts +2 -1
- 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 +50 -14
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- 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/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +16 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +9 -1
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +73 -43
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- 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 +261 -81
- 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 +313 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +24 -1
- package/src/server/responses/core-combo.ts +28 -2
- package/src/server/responses/core-errors.ts +29 -1
- 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 +48 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +20 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -7
- package/src/server/responses/request-prepare.ts +74 -9
- package/src/server/responses/request-transport.ts +22 -8
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +89 -5
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +38 -4
- package/src/server/startup-health-cache.ts +5 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +161 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +466 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +71 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +68 -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 +16 -0
- package/src/usage/log.ts +19 -1
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
- package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
|
@@ -44,6 +44,10 @@ export function overlayActivationHint(configDir = getConfigDir()): string {
|
|
|
44
44
|
function sameId(left: OverlayIdentity | null | undefined, right: OverlayIdentity | null | undefined): boolean {
|
|
45
45
|
return !!left && !!right && left.dev === right.dev && left.ino === right.ino;
|
|
46
46
|
}
|
|
47
|
+
/** APFS device numbers can change after reboot; persisted ownership still requires the same inode. */
|
|
48
|
+
function samePersistedInode(current: OverlayIdentity, recorded: OverlayIdentity | undefined): boolean {
|
|
49
|
+
return !!recorded && current.ino === recorded.ino;
|
|
50
|
+
}
|
|
47
51
|
function lexical(path: string): Stats | null {
|
|
48
52
|
try { return lstatSync(path); }
|
|
49
53
|
catch (error) { if (fileErrorCode(error) === "ENOENT") return null; throw error; }
|
|
@@ -128,7 +132,7 @@ function shellEnvironment(): string {
|
|
|
128
132
|
'unset ocx_shim_dir ocx_shim_rest ocx_shim_path ocx_shim_more ocx_shim_part', ""].join("\n");
|
|
129
133
|
}
|
|
130
134
|
function ownedWrapper(value: Snapshot, state: OverlayState): boolean {
|
|
131
|
-
return
|
|
135
|
+
return samePersistedInode(value.identity, state.wrapperIdentity)
|
|
132
136
|
&& value.content.includes(SHIM_MARKER) && value.content.includes(`exec ${shQuote(state.launcherPath)} "$@"`);
|
|
133
137
|
}
|
|
134
138
|
function requireOverlayIdentities(state: OverlayState): void {
|
|
@@ -137,7 +141,7 @@ function requireOverlayIdentities(state: OverlayState): void {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
function ownedEnvironment(value: Snapshot, state: OverlayState): boolean {
|
|
140
|
-
return
|
|
144
|
+
return samePersistedInode(value.identity, state.envIdentity) && value.content === shellEnvironment();
|
|
141
145
|
}
|
|
142
146
|
function revalidateOverlay(state: OverlayState, saved: Snapshot | null, wrapper: Snapshot | null, env: Snapshot | null): void {
|
|
143
147
|
if (!unchanged(statePath(), saved) || !unchanged(state.wrapperPath, wrapper) || !unchanged(overlayPaths().env, env)) {
|
|
@@ -201,6 +205,7 @@ export function overlayDiagnostic(state: ShimState): CodexShimDiagnostic {
|
|
|
201
205
|
inspection?.revalidate();
|
|
202
206
|
} catch (error) { active = null; runnable = false; integrityMessage = failure(error).message; }
|
|
203
207
|
return { installed: true, healthy: complete && runnable && active === true, runnable, active,
|
|
208
|
+
activationPending: complete && runnable && active !== true,
|
|
204
209
|
summary: `Codex PATH shim: ${complete && runnable ? "ready" : "unhealthy"} at ${state.wrapperPath}; launcher ${state.launcherPath}; PATH ${active === null ? "unverified" : active ? "active" : "inactive"}. ${complete && runnable ? overlayActivationHint() : integrityMessage || "Run ocx codex-shim install to repair the shim."}` };
|
|
205
210
|
}
|
|
206
211
|
|
|
@@ -404,7 +409,9 @@ function failure(error: unknown): InstallResult {
|
|
|
404
409
|
const message = error instanceof Error ? error.message : "Codex overlay operation failed";
|
|
405
410
|
return { installed: false, refused: true, message: truncateRetainedUtf8(message.replace(/[\r\n]/g, " "), MAX_DIAGNOSTIC_VALUE_BYTES) };
|
|
406
411
|
}
|
|
407
|
-
function installLocked(
|
|
412
|
+
function installLocked(
|
|
413
|
+
discover: () => string | null, automatic: boolean, check: () => void, describeMiss?: () => string | null,
|
|
414
|
+
): InstallResult {
|
|
408
415
|
recoverJournal(check);
|
|
409
416
|
const pinnedState = snapshot(statePath());
|
|
410
417
|
const result = readStateResult();
|
|
@@ -428,18 +435,20 @@ function installLocked(discover: () => string | null, automatic: boolean, check:
|
|
|
428
435
|
const discovered = automatic ? null : discover();
|
|
429
436
|
launcher = discovered ? resolve(discovered) : null;
|
|
430
437
|
}
|
|
431
|
-
|
|
438
|
+
// A discovery that refused a candidate (fnm's temporary multishell path) says why; reporting it
|
|
439
|
+
// as plain absence sent users to reinstall a Codex that was already there (#6963).
|
|
440
|
+
if (!launcher) throw new Error(describeMiss?.() || "Native Codex launcher not found; install or repair Codex with its package manager");
|
|
432
441
|
check();
|
|
433
442
|
return publishOverlay(launcher, overlay, check, pinnedState);
|
|
434
443
|
}
|
|
435
|
-
export function installUnixOverlay(discover: () => string | null): InstallResult {
|
|
444
|
+
export function installUnixOverlay(discover: () => string | null, describeMiss?: () => string | null): InstallResult {
|
|
436
445
|
let lock: ReturnType<typeof tryAcquireShimRestoreLock> = null;
|
|
437
446
|
try {
|
|
438
447
|
const check = namespace(true);
|
|
439
448
|
lock = tryAcquireShimRestoreLock();
|
|
440
449
|
if (!lock) throw new Error("Codex shim operation is already in progress; retry later");
|
|
441
450
|
check();
|
|
442
|
-
return installLocked(discover, false, check);
|
|
451
|
+
return installLocked(discover, false, check, describeMiss);
|
|
443
452
|
} catch (error) { return failure(error); }
|
|
444
453
|
finally { lock?.release(); }
|
|
445
454
|
}
|
package/src/codex/shim.ts
CHANGED
|
@@ -1079,7 +1079,7 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1079
1079
|
}
|
|
1080
1080
|
|
|
1081
1081
|
export function installCodexShim(deps: RuntimePreflightDeps = {}): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
|
|
1082
|
-
if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
|
|
1082
|
+
if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath, lastCodexDiscoveryError);
|
|
1083
1083
|
return installCodexShimInternal({ ...deps, allowFreshInstall: true });
|
|
1084
1084
|
}
|
|
1085
1085
|
|
package/src/combos/failover.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseResetCooldownMs } from "../codex/routing";
|
|
2
2
|
import { classifyError, isCyberPolicyCode } from "../lib/errors";
|
|
3
3
|
import { isNonReplayableUpstreamCode } from "../lib/upstream-retry";
|
|
4
|
+
import { isAnthropicBillingFailure } from "../providers/anthropic-billing-refusal";
|
|
4
5
|
import type { OcxComboTarget } from "../types";
|
|
5
6
|
import { targetKey } from "./types";
|
|
6
7
|
import {
|
|
@@ -563,6 +564,7 @@ export function comboFailureCooldownScope(
|
|
|
563
564
|
options?: { code?: string | null },
|
|
564
565
|
): ComboFailureCooldownScope {
|
|
565
566
|
const code = normalizedFailureCode(options?.code);
|
|
567
|
+
if (isAnthropicBillingFailure(status, message, options?.code)) return "provider";
|
|
566
568
|
// Request-shape refusals first: an oversized request must not cool a healthy target.
|
|
567
569
|
// A native transport can surface a zero-output model overflow as a generic
|
|
568
570
|
// upstream_server_error carrying precise context-window prose, so consult the bounded
|
|
@@ -847,6 +849,7 @@ export function comboFailureDecision(
|
|
|
847
849
|
// reaches that list and returns terminal before anything can ask whether the next target
|
|
848
850
|
// could have served the request (#4903).
|
|
849
851
|
if (isResponseFormatCapabilityRefusal(status, message, options?.code)) return "hop";
|
|
852
|
+
if (isAnthropicBillingFailure(status, message, options?.code)) return "hop";
|
|
850
853
|
if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
|
|
851
854
|
return "stop";
|
|
852
855
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type KeyRecord = Record<string, unknown> & { id: string };
|
|
2
|
+
|
|
3
|
+
function index(value: unknown): Map<string, KeyRecord> | null {
|
|
4
|
+
if (value === undefined) return new Map();
|
|
5
|
+
if (!Array.isArray(value)) return null;
|
|
6
|
+
const rows = new Map<string, KeyRecord>();
|
|
7
|
+
for (const row of value) {
|
|
8
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.id !== "string" || !row.id || rows.has(row.id)) return null;
|
|
9
|
+
rows.set(row.id, row);
|
|
10
|
+
}
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Stable-ID three-way merge: either side's revocation defeats stale edits to that ID. */
|
|
15
|
+
export function reconcileApiKeys(
|
|
16
|
+
baseline: unknown, live: unknown, persisted: unknown,
|
|
17
|
+
mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, persisted: Record<string, unknown>) => void,
|
|
18
|
+
): unknown[] | null {
|
|
19
|
+
if (baseline === undefined && live === undefined && persisted === undefined) return null;
|
|
20
|
+
const before = index(baseline), pending = index(live), disk = index(persisted);
|
|
21
|
+
if (!before || !pending || !disk) return null;
|
|
22
|
+
const merged: KeyRecord[] = [];
|
|
23
|
+
const liveOrder = [...pending.keys()], diskOrder = [...disk.keys()];
|
|
24
|
+
const order = JSON.stringify(liveOrder) === JSON.stringify([...before.keys()])
|
|
25
|
+
? [...diskOrder, ...liveOrder] : [...liveOrder, ...diskOrder];
|
|
26
|
+
for (const id of new Set(order)) {
|
|
27
|
+
const local = pending.get(id), saved = disk.get(id);
|
|
28
|
+
if (before.has(id) && (!local || !saved)) continue;
|
|
29
|
+
const row = structuredClone(local ?? saved!);
|
|
30
|
+
if (local && saved) mergeRecord(row, before.get(id) ?? {}, saved);
|
|
31
|
+
merged.push(row);
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
@@ -6,6 +6,7 @@ import { lstatSync, readFileSync } from "node:fs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
|
|
9
|
+
import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
|
|
9
10
|
import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
|
|
10
11
|
import type { OcxConfig } from "../types";
|
|
11
12
|
import { anthropicAccountPoolConfigError, anthropicOAuthInstanceConfigError, anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
|
|
@@ -648,6 +649,21 @@ export function metricsExportConfigError(value: unknown): string | null {
|
|
|
648
649
|
return null;
|
|
649
650
|
}
|
|
650
651
|
|
|
652
|
+
/** Load degrades a malformed service health recovery section to off; live writes reject it. */
|
|
653
|
+
export function serviceHealthRecoveryConfigError(value: unknown): string | null {
|
|
654
|
+
const raw = rawConfigRecord(value);
|
|
655
|
+
if (!raw || !Object.hasOwn(raw, "serviceHealthRecovery") || raw.serviceHealthRecovery === undefined) return null;
|
|
656
|
+
const section = rawConfigRecord(raw.serviceHealthRecovery);
|
|
657
|
+
if (!section) return "schema_invalid: serviceHealthRecovery: must be an object or omitted";
|
|
658
|
+
if (Object.keys(section).some(key => key !== "enabled")) {
|
|
659
|
+
return "schema_invalid: serviceHealthRecovery: contains an unsupported field";
|
|
660
|
+
}
|
|
661
|
+
if (section.enabled !== undefined && typeof section.enabled !== "boolean") {
|
|
662
|
+
return "schema_invalid: serviceHealthRecovery.enabled: must be a boolean";
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
|
|
651
667
|
|
|
652
668
|
function skillsConfigError(value: unknown): string | null {
|
|
653
669
|
const raw = rawConfigRecord(value);
|
|
@@ -679,7 +695,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
679
695
|
const anthropicError = anthropicOAuthInstanceConfigError(value) ?? anthropicAccountPoolConfigError(value) ?? anthropicSidecarConfigError(value);
|
|
680
696
|
if (anthropicError) return { ok: false, error: anthropicError };
|
|
681
697
|
const boundaryError = blockedModelRedirectsError(value)
|
|
682
|
-
?? compactionRecoveryConfigError(value) ?? configReasoningPinsConfigError(value)
|
|
698
|
+
?? compactionRecoveryConfigError(value) ?? desktopCompatibilityConfigError(value) ?? configReasoningPinsConfigError(value)
|
|
683
699
|
?? blankHostnameError(value)
|
|
684
700
|
?? (rawSubagentModelForce(value) !== undefined && !isSubagentModelEntry(rawSubagentModelForce(value)) ? "schema_invalid: claudeCode.subagentModelForce: expected a safe roster-style model id" : null)
|
|
685
701
|
?? claudeSubagentEffortError(value)
|
|
@@ -710,6 +726,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
710
726
|
?? loopbackListenerPortError(value)
|
|
711
727
|
?? managementIngressConfigError(value)
|
|
712
728
|
?? metricsExportConfigError(value)
|
|
729
|
+
?? serviceHealthRecoveryConfigError(value)
|
|
713
730
|
?? skillsConfigError(value);
|
|
714
731
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
715
732
|
const result = configSchema.safeParse(value);
|
|
@@ -17,6 +17,11 @@ export function metricsExportEnabled(config: Pick<OcxConfig, "metricsExport">):
|
|
|
17
17
|
return config.metricsExport?.enabled === true;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Default-off service health recovery (#6671); read once when the proxy starts. */
|
|
21
|
+
export function serviceHealthRecoveryEnabled(config: Pick<OcxConfig, "serviceHealthRecovery">): boolean {
|
|
22
|
+
return config.serviceHealthRecovery?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Default cadence for catalog auto-refresh (issue #3630): one converge pass
|
|
22
27
|
* per hour. Each pass spends a live /models call against every enabled provider, and
|
|
@@ -18,6 +18,8 @@ import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } f
|
|
|
18
18
|
import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
|
|
19
19
|
import { configDiagnosticsFromRaw, readConfigDiagnostics } from "./diagnostics";
|
|
20
20
|
import { normalizePersistedClaudeCode } from "./load-degrade";
|
|
21
|
+
import { reconcileApiKeys } from "./api-key-reconcile";
|
|
22
|
+
import { captureLiveTopologyBaseline, LIVE_TOPOLOGY_KEYS, projectLiveTopology } from "./live-topology";
|
|
21
23
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// Hand-edit protection for the `claudeCode` subtree (devlog 260726_claude_auth_auto/040 H1).
|
|
@@ -104,6 +106,7 @@ const detachedConfigSnapshots = new WeakSet<OcxConfig>();
|
|
|
104
106
|
export function armClaudeCodeBaseline(config: OcxConfig): void {
|
|
105
107
|
liveConfigBaseline.set(config, structuredClone(config));
|
|
106
108
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
109
|
+
captureLiveTopologyBaseline(config);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
@@ -135,6 +138,19 @@ export function adoptPersistedProviderIntoLiveConfig(
|
|
|
135
138
|
if (persistedConfig) refreshPreservedProviderOwner(config, persistedConfig);
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
/** Adopt only the committed next-start preference; preserve unrelated pending live edits. */
|
|
142
|
+
export function adoptPersistedDesktopCompatibility(config: OcxConfig, persisted: OcxConfig["desktopCompatibility"]): void {
|
|
143
|
+
const baseline = liveConfigBaseline.get(config);
|
|
144
|
+
const merged = reconcileConfigValue(baseline?.desktopCompatibility ?? MISSING_CONFIG_VALUE,
|
|
145
|
+
config.desktopCompatibility ?? MISSING_CONFIG_VALUE, persisted ?? MISSING_CONFIG_VALUE);
|
|
146
|
+
if (merged === MISSING_CONFIG_VALUE) delete config.desktopCompatibility;
|
|
147
|
+
else config.desktopCompatibility = merged as OcxConfig["desktopCompatibility"];
|
|
148
|
+
if (baseline) {
|
|
149
|
+
if (persisted === undefined) delete baseline.desktopCompatibility;
|
|
150
|
+
else baseline.desktopCompatibility = structuredClone(persisted);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
138
154
|
/** Test seam only: is this instance armed? */
|
|
139
155
|
export function claudeCodeBaselineArmed(config: OcxConfig): boolean {
|
|
140
156
|
return claudeCodeBaseline.has(config);
|
|
@@ -321,7 +337,12 @@ function reconcileConfigRecord(
|
|
|
321
337
|
&& baselineValue !== MISSING_CONFIG_VALUE
|
|
322
338
|
&& persistedValue === MISSING_CONFIG_VALUE
|
|
323
339
|
? MISSING_CONFIG_VALUE
|
|
324
|
-
: key === "
|
|
340
|
+
: key === "apiKeys"
|
|
341
|
+
? reconcileApiKeys(baselineValue === MISSING_CONFIG_VALUE ? undefined : baselineValue,
|
|
342
|
+
liveValue === MISSING_CONFIG_VALUE ? undefined : liveValue,
|
|
343
|
+
persistedValue === MISSING_CONFIG_VALUE ? undefined : persistedValue, reconcileConfigRecord)
|
|
344
|
+
?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
|
|
345
|
+
: key === "customModels"
|
|
325
346
|
? reconcileCustomModels(baselineValue, liveValue, persistedValue)
|
|
326
347
|
?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
|
|
327
348
|
: key === "disabledModels"
|
|
@@ -401,7 +422,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
|
|
|
401
422
|
config as unknown as Record<string, unknown>,
|
|
402
423
|
persistedBaseline as unknown as Record<string, unknown>,
|
|
403
424
|
persisted as unknown as Record<string, unknown>,
|
|
404
|
-
new Set([
|
|
425
|
+
new Set([...LIVE_TOPOLOGY_KEYS, ...(claudeGuardArmed ? ["claudeCode"] : [])]),
|
|
405
426
|
);
|
|
406
427
|
applyConfigObjectChildDeletions(config, childDeletions);
|
|
407
428
|
|
|
@@ -473,36 +494,32 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
473
494
|
// custom-model deletion migration. A second read could observe different bytes.
|
|
474
495
|
const onDisk = readRawConfigJson();
|
|
475
496
|
const baseline = liveConfigBaseline.get(config);
|
|
497
|
+
let diskDeletedKeys = new Set<string>();
|
|
498
|
+
let persistedSnapshot: OcxConfig | undefined;
|
|
476
499
|
if (baseline && onDisk !== undefined) {
|
|
477
500
|
const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
|
|
478
501
|
if (persistedDiagnostics.source === "file") {
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// key — including configRebaseProvenance, so a cooperating writer's
|
|
484
|
-
// deletion marker adopted from disk is honored instead of silently dropped —
|
|
485
|
-
// and captures deletion intent from the current disk snapshot before the
|
|
486
|
-
// merge can temporarily restore a key and invalidate its deletion marker.
|
|
502
|
+
persistedSnapshot = persistedDiagnostics.config;
|
|
503
|
+
// Capture authoritative disk deletions before reconciliation can restore a key.
|
|
504
|
+
// Live instances retain actual socket identity; detached discovery snapshots
|
|
505
|
+
// own no socket and also merge listener fields and disk-only sections.
|
|
487
506
|
const detached = detachedConfigSnapshots.has(config);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
507
|
+
diskDeletedKeys = configRebaseDeletionKeys(persistedDiagnostics.config);
|
|
508
|
+
// Preserve explicit pending live edits; stale retention is not an edit.
|
|
509
|
+
if (!detached) for (const key of diskDeletedKeys) {
|
|
510
|
+
if (!LIVE_TOPOLOGY_KEYS.has(key) && !deepEqual(
|
|
511
|
+
ownConfigValue(config as unknown as Record<string, unknown>, key),
|
|
512
|
+
ownConfigValue(baseline as unknown as Record<string, unknown>, key))) diskDeletedKeys.delete(key);
|
|
513
|
+
}
|
|
514
|
+
const deletedKeys = new Set([...diskDeletedKeys, ...configRebaseDeletionKeys(config)]);
|
|
491
515
|
const skipped = detached
|
|
492
516
|
? new Set(["claudeCode"])
|
|
493
|
-
: new Set([
|
|
517
|
+
: new Set([...LIVE_TOPOLOGY_KEYS, "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
|
|
494
518
|
if (!detached) {
|
|
495
|
-
const provenanceExists = configHasRebaseProvenance(config);
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
// another process — is absent from the baseline as well as from the live
|
|
500
|
-
// config. Reconciling those keys reads "live never changed this" and adopts
|
|
501
|
-
// the disk value, which resurrects a field the live writer had deliberately
|
|
502
|
-
// deleted (#1462 regression: PUT /api/grok/selection with an empty list).
|
|
503
|
-
// Restrict the merge to keys the baseline knew about, plus keys the live
|
|
504
|
-
// config still carries; a key that exists only on disk is left to the
|
|
505
|
-
// ordinary whole-config write below.
|
|
519
|
+
const provenanceExists = configHasRebaseProvenance(config) || configHasRebaseProvenance(persistedDiagnostics.config);
|
|
520
|
+
// Without provenance, disk-only keys remain subject to the legacy whole-
|
|
521
|
+
// config write: adopting them could resurrect a deliberate live deletion
|
|
522
|
+
// absent from its baseline (#1462). Provenance makes that intent explicit.
|
|
506
523
|
const rebaseableKeys = new Set([
|
|
507
524
|
...Object.keys(baseline as unknown as Record<string, unknown>),
|
|
508
525
|
...Object.keys(config as unknown as Record<string, unknown>),
|
|
@@ -521,6 +538,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
521
538
|
skipped,
|
|
522
539
|
);
|
|
523
540
|
for (const key of deletedKeys) {
|
|
541
|
+
if (!detached && LIVE_TOPOLOGY_KEYS.has(key)) continue;
|
|
524
542
|
delete (config as unknown as Record<string, unknown>)[key];
|
|
525
543
|
}
|
|
526
544
|
}
|
|
@@ -537,15 +555,23 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
537
555
|
}
|
|
538
556
|
}
|
|
539
557
|
}
|
|
540
|
-
const
|
|
558
|
+
const desired = persistedSnapshot && !detachedConfigSnapshots.has(config)
|
|
559
|
+
? projectLiveTopology(config, persistedSnapshot, reconcileConfigRecord) : config;
|
|
560
|
+
const durableProjection = { ...desired, configRebaseProvenance: projectConfigRebaseProvenance(config).configRebaseProvenance };
|
|
561
|
+
for (const key of diskDeletedKeys) {
|
|
562
|
+
delete (durableProjection as unknown as Record<string, unknown>)[key];
|
|
563
|
+
}
|
|
564
|
+
if (diskDeletedKeys.size) durableProjection.configRebaseProvenance = { version: 1,
|
|
565
|
+
deletedTopLevelKeys: [...new Set([...configRebaseDeletionKeys(durableProjection), ...diskDeletedKeys])].sort() };
|
|
566
|
+
const provenanceProjection = projectConfigRebaseProvenance(durableProjection);
|
|
541
567
|
const projectedConfig = projectCustomModelCatalogMigration(
|
|
542
568
|
onDisk,
|
|
543
|
-
|
|
569
|
+
durableProjection,
|
|
544
570
|
);
|
|
545
571
|
if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
|
|
546
572
|
else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
547
|
-
const persistedBinding = bindingBaseline && onDisk
|
|
548
|
-
? readPersistedServerBinding(onDisk, bindingBaseline)
|
|
573
|
+
const persistedBinding = (bindingBaseline || diskDeletedKeys.has("hostname")) && onDisk
|
|
574
|
+
? readPersistedServerBinding(onDisk, bindingBaseline ?? { port: config.port, hostname: config.hostname })
|
|
549
575
|
: bindingBaseline;
|
|
550
576
|
if (persistedBinding) {
|
|
551
577
|
const persistedConfig: OcxConfig = { ...projectedConfig, port: persistedBinding.port };
|
|
@@ -557,6 +583,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
557
583
|
persist(projectedConfig);
|
|
558
584
|
}
|
|
559
585
|
adoptCustomModelCatalogMigration(config, projectedConfig);
|
|
586
|
+
captureLiveTopologyBaseline(config);
|
|
560
587
|
if (claudeCodeBaseline.has(config)) {
|
|
561
588
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
562
589
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const LIVE_TOPOLOGY_KEYS = new Set(["hostname", "port", "runtimeRole", "hub", "client", "unauthenticatedLoopbackListener"]);
|
|
4
|
+
const baselines = new WeakMap<OcxConfig, Record<string, unknown>>();
|
|
5
|
+
|
|
6
|
+
function snapshot(config: OcxConfig): Record<string, unknown> {
|
|
7
|
+
const record = config as unknown as Record<string, unknown>;
|
|
8
|
+
return Object.fromEntries([...LIVE_TOPOLOGY_KEYS].filter(key => Object.hasOwn(record, key))
|
|
9
|
+
.map(key => [key, structuredClone(record[key])]));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Track actual active values independently of the desired config last persisted. */
|
|
13
|
+
export function captureLiveTopologyBaseline(config: OcxConfig): void {
|
|
14
|
+
baselines.set(config, snapshot(config));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Merge desired next-start topology without mutating the active listener/role objects. */
|
|
18
|
+
export function projectLiveTopology(
|
|
19
|
+
config: OcxConfig, persisted: OcxConfig,
|
|
20
|
+
mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, disk: Record<string, unknown>, skipped: ReadonlySet<string>) => void,
|
|
21
|
+
): OcxConfig {
|
|
22
|
+
const projected = { ...config, ...snapshot(config) } as unknown as Record<string, unknown>;
|
|
23
|
+
const baseline = baselines.get(config) ?? snapshot(config);
|
|
24
|
+
const disk = persisted as unknown as Record<string, unknown>;
|
|
25
|
+
const skipped = new Set([...Object.keys(projected), ...Object.keys(baseline), ...Object.keys(disk)]
|
|
26
|
+
.filter(key => !LIVE_TOPOLOGY_KEYS.has(key)));
|
|
27
|
+
mergeRecord(projected, baseline, disk, skipped);
|
|
28
|
+
return projected as unknown as OcxConfig;
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@ import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
|
|
|
3
3
|
import { chmodSync, existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { compactionRecoveryConfigError } from "./schema/compaction-recovery";
|
|
6
|
+
import { desktopCompatibilityConfigError } from "./schema/desktop-compatibility";
|
|
6
7
|
import { blockedModelRedirectsError } from "./schema/blocked-model-redirects";
|
|
7
8
|
import { anthropicSidecarConfigError } from "./schema/anthropic-account-pool";
|
|
8
9
|
import {
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
} from "./provider-validation";
|
|
17
18
|
import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
|
|
18
19
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
20
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
19
21
|
import { redactSecretString } from "../lib/redact";
|
|
20
22
|
import { isValidProviderName } from "./provider-name";
|
|
21
23
|
import { MODEL_ALIAS_PATTERN } from "../providers/default-aliases";
|
|
@@ -49,10 +51,13 @@ import {
|
|
|
49
51
|
import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
|
|
50
52
|
|
|
51
53
|
export function hardenExistingSecret(path: string): void {
|
|
52
|
-
if (existsSync(path)) {
|
|
53
|
-
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
54
|
+
if (existsSync(path) && !isPassiveRead()) {
|
|
54
55
|
if (process.platform === "win32") {
|
|
56
|
+
// Preserve the ACL helper's metadata observation across reads. Windows chmod does
|
|
57
|
+
// not establish a private DACL and can invalidate that observation before every load.
|
|
55
58
|
hardenSecretPath(path, { required: false });
|
|
59
|
+
} else {
|
|
60
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
}
|
|
@@ -128,6 +133,7 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
|
|
|
128
133
|
export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
|
|
129
134
|
setCodexWsReuseAcrossTurns(validated.codexWsReuseAcrossTurns === true);
|
|
130
135
|
if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
|
|
136
|
+
if (desktopCompatibilityConfigError(rawParsed)) console.warn("Invalid desktopCompatibility startup preference ignored; no desktop compatibility service will start automatically.");
|
|
131
137
|
if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
|
|
132
138
|
const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
|
|
133
139
|
if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
package/src/config/paths.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { flushWindowsSecretAclReapsBeforeRemoval, hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
|
|
5
5
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
6
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
7
|
+
import { trackBackgroundWork } from "../lib/background-work";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Expand a leading `~` in user-supplied paths without interpreting shell
|
|
@@ -29,22 +31,30 @@ export function getConfigPath(): string {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function hardenConfigDir(): void {
|
|
34
|
+
// A passive read (a Remote Link Child's, #6959) repairs nothing on this machine.
|
|
35
|
+
if (isPassiveRead()) return;
|
|
32
36
|
const dir = getConfigDir();
|
|
33
37
|
// The guard runs before any mutation: refusing after chmod/ACL would already
|
|
34
38
|
// have changed the protected directory used by the test-home boundary.
|
|
35
39
|
assertNotRealHomeUnderTest(dir);
|
|
36
40
|
if (!existsSync(dir)) return;
|
|
37
|
-
|
|
41
|
+
// Windows privacy is enforced by NTFS ACLs. chmod only changes DOS attributes there;
|
|
42
|
+
// doing it before the ACL lookup can invalidate its ctime-based successful observation.
|
|
43
|
+
if (!windowsSecretAclApplies()) {
|
|
44
|
+
try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
|
|
45
|
+
}
|
|
38
46
|
if (windowsSecretAclApplies() && !configDirHardeningFlights.has(dir)) {
|
|
39
47
|
// This is an optional read-path harden. Waiting synchronously here used to stop the Bun
|
|
40
48
|
// event loop (including /healthz) for the full icacls timeout. Required mutation paths keep
|
|
41
49
|
// their own awaited/fail-closed hardening; ordinary config reads only start one soft flight.
|
|
42
|
-
|
|
50
|
+
// Tracked: icacls holds the directory open, and a config write or removal that lands on
|
|
51
|
+
// it before the child exits fails with a sharing violation on Windows.
|
|
52
|
+
const flight = trackBackgroundWork("config-dir-acl-harden", hardenSecretDirAsync(dir, { required: false })
|
|
43
53
|
.then(() => undefined)
|
|
44
54
|
.catch(() => undefined)
|
|
45
55
|
.finally(() => {
|
|
46
56
|
if (configDirHardeningFlights.get(dir) === flight) configDirHardeningFlights.delete(dir);
|
|
47
|
-
});
|
|
57
|
+
}));
|
|
48
58
|
configDirHardeningFlights.set(dir, flight);
|
|
49
59
|
}
|
|
50
60
|
}
|
package/src/config/salvage.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import * as z from "zod/v4";
|
|
4
4
|
import { CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR } from "../codex/account-namespace-match";
|
|
5
5
|
import { redactSecretString } from "../lib/redact";
|
|
6
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
6
7
|
import { hasWarnedConfigFallback, markWarnedConfigFallback } from "./warn-memo";
|
|
7
8
|
import { configSchema } from "./schema/config-schema";
|
|
8
9
|
import type { OcxConfig } from "../types";
|
|
@@ -232,7 +233,8 @@ export function warnAndBackupInvalidConfig(configPath: string, error: unknown):
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
export function backupInvalidConfig(configPath: string): string | null {
|
|
235
|
-
|
|
236
|
+
// A passive read leaves a malformed file for the Home's own load to back up (#6959).
|
|
237
|
+
if (!existsSync(configPath) || isPassiveRead()) return null;
|
|
236
238
|
const backupPath = `${configPath}.invalid-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
237
239
|
try {
|
|
238
240
|
copyFileSync(configPath, backupPath);
|
|
@@ -2,6 +2,7 @@ import { MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT } from "../../codex/quota-types";
|
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
3
|
import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
|
|
4
4
|
import { compactionRecoverySchema } from "./compaction-recovery";
|
|
5
|
+
import { desktopCompatibilitySchema } from "./desktop-compatibility";
|
|
5
6
|
import { blockedModelRedirectsSchema } from "./blocked-model-redirects";
|
|
6
7
|
import {
|
|
7
8
|
agentTaskRecoverySchema,
|
|
@@ -83,6 +84,7 @@ export const protocolConfigSchema = z.object({
|
|
|
83
84
|
managedMessagesNative: z.boolean().optional(),
|
|
84
85
|
managedMessagesNativeOAuth: z.boolean().optional(),
|
|
85
86
|
directEncoders: z.boolean().optional(),
|
|
87
|
+
antigravityDirect: z.boolean().optional(),
|
|
86
88
|
shadowPlan: z.boolean().optional(),
|
|
87
89
|
}).strict().optional(),
|
|
88
90
|
}).strict();
|
|
@@ -105,6 +107,8 @@ export const configSchema = z.object({
|
|
|
105
107
|
skills: skillsConfigSchema.optional().catch(undefined),
|
|
106
108
|
// Malformed hand edits disable this opt-in exporter. Live writes reject them in diagnostics.ts.
|
|
107
109
|
metricsExport: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
110
|
+
// Same contract as metricsExport: a malformed hand edit leaves recovery off; live writes reject it.
|
|
111
|
+
serviceHealthRecovery: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
108
112
|
// Kept raw on purpose: `.catch(undefined)` would turn a mistyped `enabled` into "inherit",
|
|
109
113
|
// which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
|
|
110
114
|
// and fails closed instead.
|
|
@@ -122,6 +126,7 @@ export const configSchema = z.object({
|
|
|
122
126
|
managedMessagesNative: false,
|
|
123
127
|
managedMessagesNativeOAuth: false,
|
|
124
128
|
directEncoders: fields.directEncoders === true,
|
|
129
|
+
antigravityDirect: fields.antigravityDirect === true,
|
|
125
130
|
shadowPlan: fields.shadowPlan === true,
|
|
126
131
|
},
|
|
127
132
|
};
|
|
@@ -188,6 +193,7 @@ export const configSchema = z.object({
|
|
|
188
193
|
modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
|
|
189
194
|
compactionRouting: compactionRoutingSchema.optional().catch(undefined),
|
|
190
195
|
compactionRecovery: compactionRecoverySchema.optional().catch(undefined),
|
|
196
|
+
desktopCompatibility: desktopCompatibilitySchema.optional().catch(undefined),
|
|
191
197
|
// A hand-edited malformed phase disables only that phase instead of rejecting
|
|
192
198
|
// providers/apiKeys, matching the load-time degradation notice; the management write
|
|
193
199
|
// boundary (validateConfigCandidate) still refuses the bad value through the shared,
|
|
@@ -215,6 +221,8 @@ export const configSchema = z.object({
|
|
|
215
221
|
short: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
|
|
216
222
|
long: z.number().int().min(MAIN_ACCOUNT_HARD_LOCK_MIN_PERCENT).max(100).optional().catch(undefined),
|
|
217
223
|
}).optional().catch(undefined),
|
|
224
|
+
// #2999: a malformed hand edit falls back to auto detection rather than rejecting the config.
|
|
225
|
+
nativeMainRefresh: z.enum(["codex", "opencodex"]).optional().catch(undefined),
|
|
218
226
|
// Future versions remain opaque through passthrough-compatible whole-config saves.
|
|
219
227
|
// Only version 1 grants deletion authority in the rebase path.
|
|
220
228
|
configRebaseProvenance: z.unknown().optional(),
|
|
@@ -676,6 +684,17 @@ export const configSchema = z.object({
|
|
|
676
684
|
message: reasoningSummaryDeliveryError,
|
|
677
685
|
});
|
|
678
686
|
}
|
|
687
|
+
const minMaxOutputError = positiveIntegerConfigError(
|
|
688
|
+
(provider as { minMaxOutputTokens?: unknown }).minMaxOutputTokens,
|
|
689
|
+
"minMaxOutputTokens",
|
|
690
|
+
);
|
|
691
|
+
if (minMaxOutputError) {
|
|
692
|
+
ctx.addIssue({
|
|
693
|
+
code: "custom",
|
|
694
|
+
path: ["providers", redactSecretString(name), "minMaxOutputTokens"],
|
|
695
|
+
message: minMaxOutputError,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
679
698
|
const defaultMaxOutputError = positiveIntegerConfigError(
|
|
680
699
|
(provider as { defaultMaxOutputTokens?: unknown }).defaultMaxOutputTokens,
|
|
681
700
|
"defaultMaxOutputTokens",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
/** Startup resumes observation only. Certificate trust, app launch and Apply are never saved here. */
|
|
3
|
+
export const desktopCompatibilitySchema = z.object({ startOnProxyStart: z.boolean() }).strict();
|
|
4
|
+
export function desktopCompatibilityConfigError(value: unknown): string | null {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6
|
+
const setting = (value as Record<string, unknown>).desktopCompatibility;
|
|
7
|
+
return setting === undefined || desktopCompatibilitySchema.safeParse(setting).success
|
|
8
|
+
? null : "schema_invalid: desktopCompatibility: requires only boolean startOnProxyStart";
|
|
9
|
+
}
|
|
@@ -331,6 +331,7 @@ export const providerConfigSchema = z.object({
|
|
|
331
331
|
responseTierAuthoritative: z.boolean().optional(),
|
|
332
332
|
fastEnabled: z.boolean().optional(),
|
|
333
333
|
supportsServiceTier: z.boolean().optional(),
|
|
334
|
+
inferContextWindow: z.boolean().optional(),
|
|
334
335
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
335
336
|
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
|
|
336
337
|
preserveResponsesReasoningContent: z.boolean().optional(),
|