@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { windowsDefaultExec, windowsDesktopAppAdapter } from "../desktop-app/windows";
|
|
2
|
+
import type { DesktopExec } from "../desktop-app/types";
|
|
3
|
+
import { activateWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "./windows-package-command";
|
|
4
|
+
export { compatibilityActivationScript, validatedCompatibilityPacUrl } from "./windows-package-command";
|
|
5
|
+
|
|
6
|
+
export interface CompatibilityLaunchIo {
|
|
7
|
+
exec?: DesktopExec;
|
|
8
|
+
platform?: string;
|
|
9
|
+
}
|
|
10
|
+
export type CompatibilityLaunchResult =
|
|
11
|
+
| { status: "started"; pid: number; packageFullName: string }
|
|
12
|
+
| { status: "refused"; reason: "unsupported_platform" | "test_environment" | "package_unavailable" | "app_running" | "process_probe_failed" | "activation_failed" };
|
|
13
|
+
|
|
14
|
+
/** Never quits an app, launches the raw EXE, changes login or installs a watcher. */
|
|
15
|
+
export function launchWindowsCodexCompatibility(pacUrl: string, io: CompatibilityLaunchIo = {}): CompatibilityLaunchResult {
|
|
16
|
+
validatedCompatibilityPacUrl(pacUrl);
|
|
17
|
+
if ((io.platform ?? process.platform) !== "win32") return { status: "refused", reason: "unsupported_platform" };
|
|
18
|
+
if (process.env.OCX_TEST_HOME_GUARD === "1" && !io.exec) return { status: "refused", reason: "test_environment" };
|
|
19
|
+
const exec = io.exec ?? windowsDefaultExec;
|
|
20
|
+
const install = windowsDesktopAppAdapter.discover(exec);
|
|
21
|
+
if (!install) return { status: "refused", reason: "package_unavailable" };
|
|
22
|
+
const processes = windowsDesktopAppAdapter.listProcesses(exec, install);
|
|
23
|
+
if (processes === null) return { status: "refused", reason: "process_probe_failed" };
|
|
24
|
+
if (processes.length > 0) return { status: "refused", reason: "app_running" };
|
|
25
|
+
try {
|
|
26
|
+
return { status: "started", ...activateWindowsCodexCompatibility(exec, install, pacUrl) };
|
|
27
|
+
} catch { return { status: "refused", reason: "activation_failed" }; }
|
|
28
|
+
}
|
|
@@ -1831,7 +1831,7 @@ export function countPendingOpencodexHistory(
|
|
|
1831
1831
|
: { pendingRows: 0, backupEntries };
|
|
1832
1832
|
}
|
|
1833
1833
|
try {
|
|
1834
|
-
const db =
|
|
1834
|
+
const db = openCodexStateForPreflight(stateDbPath);
|
|
1835
1835
|
try {
|
|
1836
1836
|
db.exec("PRAGMA busy_timeout = 100");
|
|
1837
1837
|
// Prove the expected history schema is readable without counting unowned routed rows.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AtomicWriteHooks } from "../../config/atomic-write";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { withConfigMutationLockSync } from "../../config";
|
|
4
|
-
import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
|
|
4
|
+
import { ConfigWriteLockRefusal, assertConfigWriteDestination, publishConfigWriteTarget, removeConfigWriteEntry, watchConfigWriteTargets, withConfigWriteLockHeld, type LockHandle } from "../config-write-lock";
|
|
5
5
|
import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "../paths";
|
|
6
6
|
import { getCodexHome } from "../paths";
|
|
7
7
|
import { codexHomeIsAbsent } from "../codex-home-owner";
|
|
@@ -16,6 +16,9 @@ export function publishCodexArtifact<T>(path: string, held: LockHandle | undefin
|
|
|
16
16
|
if (!held) throw new Error("Codex artifact publication requires a held configuration lock");
|
|
17
17
|
return publishConfigWriteTarget(CODEX_CONFIG_PATH, held, path, run);
|
|
18
18
|
}
|
|
19
|
+
export function removeCodexProfileEntry(held: LockHandle): void {
|
|
20
|
+
removeConfigWriteEntry(CODEX_CONFIG_PATH, held, CODEX_PROFILE_PATH);
|
|
21
|
+
}
|
|
19
22
|
/** Courtesy cleanup re-reads provider ownership inside the same config section. */
|
|
20
23
|
export function cleanExternalProviderJournal(beforeClientWrite?: () => void): void {
|
|
21
24
|
if (codexHomeIsAbsent(getCodexHome())) return;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { beginCodexWriteSection, publishCodexArtifact } from "./config-write-section";
|
|
2
|
-
import { existsSync,
|
|
1
|
+
import { beginCodexWriteSection, publishCodexArtifact, removeCodexProfileEntry } from "./config-write-section";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { normalizeStructuralWhitespace, rootAssignmentKey, rootSourceLines, sourceAssignment, sourceText } from "../toml-source-lines";
|
|
4
4
|
import { atomicWriteFile } from "../../config";
|
|
5
|
-
import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld } from "../config-write-lock";
|
|
5
|
+
import { ConfigWriteLockRefusal, configWriteLockFailureMessage, withConfigWriteLockHeld, withConfigWriteRecovery } from "../config-write-lock";
|
|
6
6
|
import type { LockHandle } from "../config-write-lock";
|
|
7
7
|
import {
|
|
8
8
|
REALTIME_WS_BASE_URL_KEY,
|
|
@@ -206,15 +206,26 @@ export function removeCodexConfig(
|
|
|
206
206
|
}
|
|
207
207
|
const locked = withConfigWriteLockHeld(CODEX_CONFIG_PATH, options.heldConfigWriteLock, held => {
|
|
208
208
|
beginCodexWriteSection(held);
|
|
209
|
+
if (!options.preserveProfile) {
|
|
210
|
+
try {
|
|
211
|
+
if (lstatSync(CODEX_PROFILE_PATH).isSymbolicLink()) {
|
|
212
|
+
return { success: false, message: "Codex profile is a symbolic link; configuration and recovery evidence were preserved." };
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
209
218
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
210
219
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
211
|
-
|
|
220
|
+
removeCodexProfileEntry(held);
|
|
212
221
|
return {
|
|
213
222
|
success: true,
|
|
214
223
|
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
215
224
|
};
|
|
216
225
|
}
|
|
217
|
-
const
|
|
226
|
+
const preimage = readFileSync(CODEX_CONFIG_PATH);
|
|
227
|
+
return withConfigWriteRecovery(CODEX_CONFIG_PATH, held, () => {
|
|
228
|
+
const rawContent = preimage.toString("utf-8");
|
|
218
229
|
// Same EOL boundary as inject: strip in LF space, write back in the file's own ending.
|
|
219
230
|
// The unchanged fast path compares in LF space so an untouched file is never rewritten.
|
|
220
231
|
const eol = dominantEol(rawContent);
|
|
@@ -248,7 +259,7 @@ export function removeCodexConfig(
|
|
|
248
259
|
publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, applyEol(finalContent, eol), undefined, hooks));
|
|
249
260
|
}
|
|
250
261
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
251
|
-
|
|
262
|
+
removeCodexProfileEntry(held);
|
|
252
263
|
const retainedNote = retainedBlock === null
|
|
253
264
|
? ""
|
|
254
265
|
: " Kept [model_providers.opencodex] so conversations already tagged opencodex still open;"
|
|
@@ -272,6 +283,7 @@ export function removeCodexConfig(
|
|
|
272
283
|
message: removedMessage,
|
|
273
284
|
...(retainedBlock === null ? {} : { retainedProviderTable: retainedBlock.replace(/\n+$/, "").split("\n") }),
|
|
274
285
|
};
|
|
286
|
+
}, { preimage, requireMatchingPreimage: true });
|
|
275
287
|
});
|
|
276
288
|
if (!locked.ok) return { success: false, retryable: locked.error === "locked", message: configWriteLockFailureMessage(locked) };
|
|
277
289
|
return locked.value;
|
package/src/codex/journal.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beginCodexWriteSection, publishCodexArtifact } from "./inject/config-write-section";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import type { AtomicWriteHooks } from "../config/atomic-write";
|
|
4
|
-
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { atomicWriteFile } from "../config";
|
|
7
7
|
import { assertCodexHomeOwner, codexHomeIsAbsent, CODEX_HOME_JOURNAL_FILE, CodexHomeOwnerRefusal, opencodexHomeForInjection, readCodexHomeJournal, type CodexHomeOwnerRefusalReason } from "./codex-home-owner";
|
|
@@ -347,6 +347,21 @@ export function restoreJournalState(
|
|
|
347
347
|
};
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// A matching byte hash does not authorize following a newly substituted profile link.
|
|
351
|
+
// Refuse the complete pair before restoring config or deleting its recovery journal.
|
|
352
|
+
let profileIsSymlink = false;
|
|
353
|
+
if (profileUnchanged && (!comparison.profileAlreadyOriginal || comparison.originalProfile === null)) {
|
|
354
|
+
try { profileIsSymlink = lstatSync(CODEX_PROFILE_PATH).isSymbolicLink(); }
|
|
355
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
356
|
+
}
|
|
357
|
+
if (profileIsSymlink) {
|
|
358
|
+
return {
|
|
359
|
+
configRestored: comparison.configAlreadyOriginal, profileRestored: false,
|
|
360
|
+
configRewritten: false, profileRewritten: false,
|
|
361
|
+
configChanged: !configUnchanged, profileChanged: false,
|
|
362
|
+
complete: false, unverified: false, profileRestoreFailed: true as const,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
350
365
|
assertCodexHomeOwner(CODEX_HOME);
|
|
351
366
|
let configRestored = comparison.configAlreadyOriginal;
|
|
352
367
|
let profileRestored = comparison.profileAlreadyOriginal;
|
|
@@ -31,6 +31,12 @@ import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch
|
|
|
31
31
|
import { withNativeMainExclusiveClaim } from "./native-main-claim";
|
|
32
32
|
import { resolveNativeProfileContext } from "./native-profile-store";
|
|
33
33
|
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
34
|
+
import { loadConfig } from "../config";
|
|
35
|
+
import {
|
|
36
|
+
requestCodexAuthStatus,
|
|
37
|
+
resolveNativeMainRefreshMode,
|
|
38
|
+
type CodexAuthStatus,
|
|
39
|
+
} from "./native-main-refresh-mode";
|
|
34
40
|
|
|
35
41
|
export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
36
42
|
|
|
@@ -48,13 +54,11 @@ type MainAuthJsonCredential = {
|
|
|
48
54
|
path: string;
|
|
49
55
|
rawSha256: string;
|
|
50
56
|
/**
|
|
51
|
-
* Filesystem identity of the file the hash was taken from
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* hash
|
|
55
|
-
*
|
|
56
|
-
* still the same file, not merely one with the same bytes. `null` when the target could
|
|
57
|
-
* not be stat'ed, which is treated as "cannot prove identity" rather than "matches".
|
|
57
|
+
* Filesystem identity of the file the hash was taken from. It catches a writer that
|
|
58
|
+
* replaces auth.json by rename with identical bytes. It does not see Codex's own writes:
|
|
59
|
+
* Codex rewrites auth.json in place (truncate + write), which keeps dev and ino, so only
|
|
60
|
+
* the content hash notices those, and only once the new bytes are visible (#2999).
|
|
61
|
+
* `null` when the target could not be stat'ed, which fails closed.
|
|
58
62
|
*/
|
|
59
63
|
identity: { dev: number; ino: number } | null;
|
|
60
64
|
root: Record<string, unknown>;
|
|
@@ -66,6 +70,8 @@ type MainAuthJsonCredential = {
|
|
|
66
70
|
|
|
67
71
|
export interface NativeMainRefreshDependencies {
|
|
68
72
|
refreshToken?: (refreshToken: string, options: { signal: AbortSignal }) => Promise<OAuthCredentials>;
|
|
73
|
+
/** Codex-mode seam: one app-server `getAuthStatus` call. Used only when Codex owns the refresh. */
|
|
74
|
+
codexAuthStatus?: (options: { refresh: boolean; signal: AbortSignal }) => Promise<CodexAuthStatus>;
|
|
69
75
|
signal?: AbortSignal;
|
|
70
76
|
/** Metadata-only refresh must not retract a concurrent traffic reauth quarantine. */
|
|
71
77
|
preserveReauth?: boolean;
|
|
@@ -264,10 +270,9 @@ function assertMainAuthJsonSnapshotUnchanged(expected: MainAuthJsonCredential):
|
|
|
264
270
|
if (!current || current.path !== expected.path || current.rawSha256 !== expected.rawSha256) {
|
|
265
271
|
throw new MainAuthJsonChangedDuringRefreshError();
|
|
266
272
|
}
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// unreadable identity on either side fails closed: unprovable is not the same as equal.
|
|
273
|
+
// Also refuse a rename-based replacement with identical bytes. An unreadable identity on
|
|
274
|
+
// either side fails closed. This narrows the window; rename(2) still replaces
|
|
275
|
+
// unconditionally after this check returns.
|
|
271
276
|
assertMainAuthJsonIdentityUnchanged(expected);
|
|
272
277
|
}
|
|
273
278
|
|
|
@@ -288,9 +293,7 @@ function persistRefreshedMainAuthJson(
|
|
|
288
293
|
assertNotRealCodexHomeUnderTest(resolveCodexHomeDir());
|
|
289
294
|
const accessToken = refreshed.access;
|
|
290
295
|
const refreshToken = refreshed.refresh || expected.refreshToken!;
|
|
291
|
-
const chatgptAccountId = refreshed
|
|
292
|
-
?? extractAccountId(undefined, accessToken)
|
|
293
|
-
?? expected.chatgptAccountId;
|
|
296
|
+
const { chatgptAccountId } = inFlightMainAccountToken(expected, refreshed);
|
|
294
297
|
const tokens = {
|
|
295
298
|
...expected.tokens,
|
|
296
299
|
access_token: accessToken,
|
|
@@ -475,6 +478,12 @@ async function resolveMainAccountToken(
|
|
|
475
478
|
const signal = dependencies.signal
|
|
476
479
|
? AbortSignal.any([dependencies.signal, refreshTimeout])
|
|
477
480
|
: refreshTimeout;
|
|
481
|
+
// An injected exchange is the guarded writer by definition. Otherwise Codex refreshes its own
|
|
482
|
+
// login whenever a supported binary exists, unless the operator chose "opencodex" (#2999).
|
|
483
|
+
const refreshMode = dependencies.refreshToken
|
|
484
|
+
? null
|
|
485
|
+
: await resolveNativeMainRefreshMode(loadConfig());
|
|
486
|
+
if (signal.aborted) throw signal.reason;
|
|
478
487
|
const lockKey = refreshGrantFingerprintForToken(initial.refreshToken);
|
|
479
488
|
// Two locks, because they guard two different things that live in two different
|
|
480
489
|
// homes. `withCodexRefreshFileLock` is keyed on the grant fingerprint and lives
|
|
@@ -508,6 +517,11 @@ async function resolveMainAccountToken(
|
|
|
508
517
|
&& mainAccessTokenFresh(locked.accessToken, Date.now(), MAIN_TOKEN_REFRESH_SKEW_MS)) {
|
|
509
518
|
return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
|
|
510
519
|
}
|
|
520
|
+
if (refreshMode?.mode === "codex") {
|
|
521
|
+
const command = refreshMode.command!;
|
|
522
|
+
return refreshThroughCodex(locked, rejectedAccessToken, dependencies, refreshTimeout,
|
|
523
|
+
dependencies.codexAuthStatus ?? (options => requestCodexAuthStatus(command, options)));
|
|
524
|
+
}
|
|
511
525
|
const refresh = dependencies.refreshToken ?? refreshNativeMainGrant;
|
|
512
526
|
let refreshed: OAuthCredentials;
|
|
513
527
|
if (signal.aborted) throw signal.reason;
|
|
@@ -539,7 +553,19 @@ async function resolveMainAccountToken(
|
|
|
539
553
|
// Once the exchange starts, only its own timeout may abort it: a successful
|
|
540
554
|
// rotating grant retires the old refresh token. Save the only live grant before
|
|
541
555
|
// honoring caller cancellation, while retaining the external-writer guards.
|
|
542
|
-
|
|
556
|
+
let result: { accessToken: string; chatgptAccountId: string };
|
|
557
|
+
try {
|
|
558
|
+
result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
559
|
+
} catch (cause) {
|
|
560
|
+
if (!(cause instanceof MainAuthJsonChangedDuringRefreshError)) throw cause;
|
|
561
|
+
// Another writer owns auth.json now (#2999). The exchange already succeeded, so
|
|
562
|
+
// failing the request would waste it. Serve the new access token to this request
|
|
563
|
+
// only: leave the file as the other writer left it, keep nothing as the main
|
|
564
|
+
// credential, and do not retire the grant.
|
|
565
|
+
noteNativeMainRefresh("publish-refused-served-in-flight");
|
|
566
|
+
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
567
|
+
return inFlightMainAccountToken(locked, refreshed);
|
|
568
|
+
}
|
|
543
569
|
// A permitted in-flight success speaks only for its own profile and grant.
|
|
544
570
|
clearMainRefreshGrantRejection(mainRefreshGrantKey(locked)!);
|
|
545
571
|
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
@@ -556,6 +582,64 @@ async function resolveMainAccountToken(
|
|
|
556
582
|
}
|
|
557
583
|
}
|
|
558
584
|
|
|
585
|
+
/** The refreshed bearer for one request whose save was refused; never stored anywhere. */
|
|
586
|
+
function inFlightMainAccountToken(
|
|
587
|
+
expected: MainAuthJsonCredential,
|
|
588
|
+
refreshed: OAuthCredentials,
|
|
589
|
+
): { accessToken: string; chatgptAccountId: string } {
|
|
590
|
+
return {
|
|
591
|
+
accessToken: refreshed.access,
|
|
592
|
+
chatgptAccountId: refreshed.accountId
|
|
593
|
+
?? extractAccountId(undefined, refreshed.access)
|
|
594
|
+
?? expected.chatgptAccountId,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Codex mode (#2999): Codex refreshes and stores its own login; OpenCodex only uses the bearer.
|
|
600
|
+
*
|
|
601
|
+
* auth.json is not read again after the call. In keyring mode Codex deletes the file once the
|
|
602
|
+
* keyring save succeeds, so the RPC answer is the only place the new token exists for us.
|
|
603
|
+
* The call runs on the refresh timeout alone: killing app-server mid-exchange could retire
|
|
604
|
+
* the grant before Codex saves its successor.
|
|
605
|
+
*/
|
|
606
|
+
async function refreshThroughCodex(
|
|
607
|
+
locked: MainAuthJsonCredential,
|
|
608
|
+
rejectedAccessToken: string | undefined,
|
|
609
|
+
dependencies: NativeMainRefreshDependencies,
|
|
610
|
+
refreshTimeout: AbortSignal,
|
|
611
|
+
authStatus: NonNullable<NativeMainRefreshDependencies["codexAuthStatus"]>,
|
|
612
|
+
): Promise<{ accessToken: string; chatgptAccountId: string }> {
|
|
613
|
+
let status: CodexAuthStatus;
|
|
614
|
+
try {
|
|
615
|
+
status = await authStatus({ refresh: true, signal: refreshTimeout });
|
|
616
|
+
} catch (cause) {
|
|
617
|
+
noteNativeMainRefresh("codex-unavailable");
|
|
618
|
+
throw new MainAccountTokenRefreshError("transient", { cause });
|
|
619
|
+
}
|
|
620
|
+
if (status.authMethod === "chatgpt" && !status.authToken) {
|
|
621
|
+
// The only permanent-failure signal the response carries: Codex withholds the token once
|
|
622
|
+
// its refresh was refused for good. The specific OAuth code is not on the wire.
|
|
623
|
+
noteNativeMainRefresh("codex-reauth");
|
|
624
|
+
markMainRefreshGrantRejected(mainRefreshGrantKey(locked)!);
|
|
625
|
+
throw new MainAccountTokenRefreshError("reauth");
|
|
626
|
+
}
|
|
627
|
+
const token = status.authToken;
|
|
628
|
+
if (status.authMethod !== "chatgpt" || !token) {
|
|
629
|
+
noteNativeMainRefresh("codex-no-chatgpt-login");
|
|
630
|
+
throw new MainAccountTokenRefreshError("transient");
|
|
631
|
+
}
|
|
632
|
+
// A transient Codex refresh failure still answers with its cached bearer.
|
|
633
|
+
if (token === rejectedAccessToken || !mainAccessTokenFresh(token, Date.now(), 0)) {
|
|
634
|
+
noteNativeMainRefresh("codex-transient");
|
|
635
|
+
throw new MainAccountTokenRefreshError("transient");
|
|
636
|
+
}
|
|
637
|
+
noteNativeMainRefresh("codex-ok");
|
|
638
|
+
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
639
|
+
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
640
|
+
return { accessToken: token, chatgptAccountId: extractAccountId(undefined, token) ?? locked.chatgptAccountId };
|
|
641
|
+
}
|
|
642
|
+
|
|
559
643
|
/** Refresh the CLI-owned native credential before upstream I/O and publish it atomically. */
|
|
560
644
|
export function getValidMainAccountToken(
|
|
561
645
|
dependencies: NativeMainRefreshDependencies = {},
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who refreshes the native main ChatGPT credential (#2999).
|
|
3
|
+
*
|
|
4
|
+
* Codex owns `CODEX_HOME/auth.json`. It rewrites the file in place and serializes its own
|
|
5
|
+
* refreshes only inside one process, so any second writer can race it, and a lost race
|
|
6
|
+
* retires the grant. In "codex" mode OpenCodex never writes the file: it asks the Codex
|
|
7
|
+
* binary to refresh through `app-server` `getAuthStatus` and uses the bearer that call
|
|
8
|
+
* returns. In "opencodex" mode the guarded writer in `main-account.ts` refreshes as before.
|
|
9
|
+
*
|
|
10
|
+
* The default is "codex" only when a version-checked binary is available. Without one, or
|
|
11
|
+
* when the operator sets `nativeMainRefresh: "opencodex"`, the guarded writer stays.
|
|
12
|
+
*/
|
|
13
|
+
import { codexExecInvocation } from "./exec-invocation";
|
|
14
|
+
import { resolveCodexHomeDir } from "./home";
|
|
15
|
+
import { compareCodexVersions, displayCodexRuntimePath, resolveCodexRuntimeAsync } from "./runtime";
|
|
16
|
+
import type { ResolveCodexRuntimeResult } from "./runtime";
|
|
17
|
+
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
18
|
+
import type { OcxConfig } from "../types/config";
|
|
19
|
+
|
|
20
|
+
export type NativeMainRefreshMode = "codex" | "opencodex";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported floor for app-server `getAuthStatus` with `includeToken`/`refreshToken` that
|
|
24
|
+
* withholds the token after a permanent refresh failure, the only reauth signal on the wire.
|
|
25
|
+
* Releases from about 0.118 already withhold it; 0.130.0 is the first with the handler in
|
|
26
|
+
* `request_processors/account_processor.rs` and is chosen conservatively.
|
|
27
|
+
*/
|
|
28
|
+
export const CODEX_AUTH_STATUS_MIN_VERSION = "0.130.0";
|
|
29
|
+
|
|
30
|
+
export type NativeMainRefreshReason =
|
|
31
|
+
| "codex-compatible"
|
|
32
|
+
| "configured-opencodex"
|
|
33
|
+
| "codex-missing"
|
|
34
|
+
| "codex-too-old"
|
|
35
|
+
| "test-guard";
|
|
36
|
+
|
|
37
|
+
export interface NativeMainRefreshResolution {
|
|
38
|
+
readonly mode: NativeMainRefreshMode;
|
|
39
|
+
readonly configured: NativeMainRefreshMode | "auto";
|
|
40
|
+
readonly reason: NativeMainRefreshReason;
|
|
41
|
+
readonly command?: string;
|
|
42
|
+
readonly version?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CodexAuthStatus {
|
|
46
|
+
readonly authMethod: string | null;
|
|
47
|
+
readonly authToken: string | null;
|
|
48
|
+
readonly requiresOpenaiAuth: boolean | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class CodexAuthStatusError extends Error {
|
|
52
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
53
|
+
super(message, options);
|
|
54
|
+
this.name = "CodexAuthStatusError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let resolutionForTests: NativeMainRefreshResolution | null = null;
|
|
59
|
+
|
|
60
|
+
/** Test seam: pin the resolution so no test spawns the developer's real Codex binary. */
|
|
61
|
+
export function setNativeMainRefreshResolutionForTests(value: NativeMainRefreshResolution | null): void {
|
|
62
|
+
resolutionForTests = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function configuredMode(config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined): NativeMainRefreshMode | "auto" {
|
|
66
|
+
const value = config?.nativeMainRefresh;
|
|
67
|
+
return value === "codex" || value === "opencodex" ? value : "auto";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function resolveNativeMainRefreshMode(
|
|
71
|
+
config: Pick<OcxConfig, "nativeMainRefresh"> | null | undefined,
|
|
72
|
+
deps: { resolveRuntime?: () => Promise<ResolveCodexRuntimeResult> } = {},
|
|
73
|
+
): Promise<NativeMainRefreshResolution> {
|
|
74
|
+
if (resolutionForTests) return resolutionForTests;
|
|
75
|
+
const configured = configuredMode(config);
|
|
76
|
+
if (configured === "opencodex") return { mode: "opencodex", configured, reason: "configured-opencodex" };
|
|
77
|
+
// Tests run against throwaway CODEX_HOMEs; delegating would spawn the real binary.
|
|
78
|
+
if (!deps.resolveRuntime && isTestHomeGuardArmed()) return { mode: "opencodex", configured, reason: "test-guard" };
|
|
79
|
+
const resolved = await (deps.resolveRuntime ?? (() => resolveCodexRuntimeAsync({ discoverAlternatives: false })))();
|
|
80
|
+
const { command, version, source } = resolved.runtime;
|
|
81
|
+
if (source === "fallback" && version === null) {
|
|
82
|
+
return { mode: "opencodex", configured, reason: "codex-missing" };
|
|
83
|
+
}
|
|
84
|
+
if (version === null || compareCodexVersions(version, CODEX_AUTH_STATUS_MIN_VERSION) < 0) {
|
|
85
|
+
return { mode: "opencodex", configured, reason: "codex-too-old", command, version };
|
|
86
|
+
}
|
|
87
|
+
return { mode: "codex", configured, reason: "codex-compatible", command, version };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* One `getAuthStatus` round trip against a short-lived `codex app-server` on stdio.
|
|
92
|
+
*
|
|
93
|
+
* `model_provider="openai"` is forced because an authless Desktop injection sets
|
|
94
|
+
* `requires_openai_auth = false` on the OpenCodex provider, and then the server reports no
|
|
95
|
+
* credential at all. The process inherits CODEX_HOME so it reads the same login (file or
|
|
96
|
+
* keyring) Codex itself uses.
|
|
97
|
+
*/
|
|
98
|
+
export async function requestCodexAuthStatus(
|
|
99
|
+
command: string,
|
|
100
|
+
options: { refresh: boolean; signal: AbortSignal; codexHome?: string },
|
|
101
|
+
): Promise<CodexAuthStatus> {
|
|
102
|
+
if (options.signal.aborted) throw options.signal.reason;
|
|
103
|
+
const invocation = codexExecInvocation(command, ["-c", 'model_provider="openai"', "app-server"]);
|
|
104
|
+
let proc: ReturnType<typeof Bun.spawn<"pipe", "pipe", "ignore">>;
|
|
105
|
+
try {
|
|
106
|
+
proc = Bun.spawn([invocation.file, ...invocation.args], {
|
|
107
|
+
env: { ...process.env, CODEX_HOME: options.codexHome ?? resolveCodexHomeDir() },
|
|
108
|
+
stdin: "pipe",
|
|
109
|
+
stdout: "pipe",
|
|
110
|
+
stderr: "ignore",
|
|
111
|
+
windowsVerbatimArguments: invocation.options.windowsVerbatimArguments,
|
|
112
|
+
windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
throw new CodexAuthStatusError("could not start codex app-server", { cause });
|
|
116
|
+
}
|
|
117
|
+
// Abort settles the call by itself. Waiting for stdout to close would hang on a child that
|
|
118
|
+
// ignores SIGTERM or a launcher descendant that keeps the pipe open.
|
|
119
|
+
let onAbort = () => {};
|
|
120
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
121
|
+
onAbort = () => reject(options.signal.reason);
|
|
122
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
123
|
+
});
|
|
124
|
+
aborted.catch(() => { /* observed through the race below */ });
|
|
125
|
+
try {
|
|
126
|
+
const send = (message: unknown) => proc.stdin.write(JSON.stringify(message) + "\n");
|
|
127
|
+
send({ id: 1, method: "initialize", params: { clientInfo: { name: "opencodex", title: null, version: "0" } } });
|
|
128
|
+
send({ method: "initialized" });
|
|
129
|
+
send({ id: 2, method: "getAuthStatus", params: { includeToken: true, refreshToken: options.refresh } });
|
|
130
|
+
await Promise.race([proc.stdin.flush(), aborted]);
|
|
131
|
+
const response = await Promise.race([readResponse(proc.stdout, 2), aborted]);
|
|
132
|
+
if (options.signal.aborted) throw options.signal.reason;
|
|
133
|
+
return parseAuthStatus(response);
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
if (options.signal.aborted) throw options.signal.reason;
|
|
136
|
+
if (cause instanceof CodexAuthStatusError) throw cause;
|
|
137
|
+
throw new CodexAuthStatusError("codex app-server getAuthStatus failed", { cause });
|
|
138
|
+
} finally {
|
|
139
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
140
|
+
stopAppServer(proc);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const APP_SERVER_KILL_GRACE_MS = 2_000;
|
|
145
|
+
|
|
146
|
+
/** SIGTERM now, SIGKILL if the child is still alive after a grace period; never awaited. */
|
|
147
|
+
function stopAppServer(proc: { stdin: { end(): unknown }; kill(signal?: number | NodeJS.Signals): void; exited: Promise<number> }): void {
|
|
148
|
+
try { proc.stdin.end(); } catch { /* already closed */ }
|
|
149
|
+
let exited = false;
|
|
150
|
+
void proc.exited.then(() => { exited = true; }, () => { exited = true; });
|
|
151
|
+
try { proc.kill("SIGTERM"); } catch { /* already gone */ }
|
|
152
|
+
const timer = setTimeout(() => {
|
|
153
|
+
if (!exited) try { proc.kill("SIGKILL"); } catch { /* already gone */ }
|
|
154
|
+
}, APP_SERVER_KILL_GRACE_MS);
|
|
155
|
+
(timer as { unref?: () => void }).unref?.();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const MAX_APP_SERVER_LINE_BYTES = 1_048_576;
|
|
159
|
+
|
|
160
|
+
async function readResponse(stream: ReadableStream<Uint8Array>, id: number): Promise<Record<string, unknown>> {
|
|
161
|
+
const decoder = new TextDecoder();
|
|
162
|
+
let buffered = "";
|
|
163
|
+
for await (const chunk of stream) {
|
|
164
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
165
|
+
let newline: number;
|
|
166
|
+
while ((newline = buffered.indexOf("\n")) >= 0) {
|
|
167
|
+
const line = buffered.slice(0, newline).trim();
|
|
168
|
+
buffered = buffered.slice(newline + 1);
|
|
169
|
+
if (!line) continue;
|
|
170
|
+
let message: unknown;
|
|
171
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
172
|
+
if (!message || typeof message !== "object") continue;
|
|
173
|
+
const record = message as Record<string, unknown>;
|
|
174
|
+
if (record.id !== id) continue;
|
|
175
|
+
if (record.error !== undefined) throw new CodexAuthStatusError("codex app-server rejected getAuthStatus");
|
|
176
|
+
if (!record.result || typeof record.result !== "object") {
|
|
177
|
+
throw new CodexAuthStatusError("codex app-server returned no getAuthStatus result");
|
|
178
|
+
}
|
|
179
|
+
return record.result as Record<string, unknown>;
|
|
180
|
+
}
|
|
181
|
+
if (buffered.length > MAX_APP_SERVER_LINE_BYTES) throw new CodexAuthStatusError("codex app-server line too long");
|
|
182
|
+
}
|
|
183
|
+
throw new CodexAuthStatusError("codex app-server exited before answering getAuthStatus");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseAuthStatus(result: Record<string, unknown>): CodexAuthStatus {
|
|
187
|
+
const authMethod = typeof result.authMethod === "string" ? result.authMethod : null;
|
|
188
|
+
const authToken = typeof result.authToken === "string" && result.authToken ? result.authToken : null;
|
|
189
|
+
const requiresOpenaiAuth = typeof result.requiresOpenaiAuth === "boolean" ? result.requiresOpenaiAuth : null;
|
|
190
|
+
return { authMethod, authToken, requiresOpenaiAuth };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `ocx doctor` lines for the active refresh owner. Never includes a credential. */
|
|
194
|
+
export function formatNativeMainRefreshDoctorLines(resolution: NativeMainRefreshResolution): string[] {
|
|
195
|
+
const configured = resolution.configured === "auto" ? "auto" : `nativeMainRefresh: "${resolution.configured}"`;
|
|
196
|
+
const binary = resolution.command
|
|
197
|
+
? `${displayCodexRuntimePath(resolution.command)} (${resolution.version ?? "unknown"})`
|
|
198
|
+
: "none";
|
|
199
|
+
switch (resolution.reason) {
|
|
200
|
+
case "codex-compatible":
|
|
201
|
+
return [` ok codex: Codex refreshes the main login through app-server (${binary}; ${configured})`];
|
|
202
|
+
case "configured-opencodex":
|
|
203
|
+
return [` -- opencodex: OpenCodex refreshes auth.json with its guarded writer (${configured})`];
|
|
204
|
+
case "test-guard":
|
|
205
|
+
return [" -- opencodex: test guard is armed; Codex delegation is disabled"];
|
|
206
|
+
case "codex-missing":
|
|
207
|
+
return [
|
|
208
|
+
` !! opencodex: no Codex binary found, so OpenCodex refreshes auth.json itself (${configured})`,
|
|
209
|
+
` Install Codex ${CODEX_AUTH_STATUS_MIN_VERSION} or newer to let Codex own its login.`,
|
|
210
|
+
];
|
|
211
|
+
case "codex-too-old":
|
|
212
|
+
return [
|
|
213
|
+
` !! opencodex: Codex ${binary} is older than ${CODEX_AUTH_STATUS_MIN_VERSION}, so OpenCodex refreshes auth.json itself (${configured})`,
|
|
214
|
+
" Update Codex to let Codex own its login.",
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -20,6 +20,7 @@ import { withNativeMainExclusiveClaim } from "./native-main-claim";
|
|
|
20
20
|
import { scrubNativeMainAuthTempResidues } from "./native-main-auth-temp";
|
|
21
21
|
import { NATIVE_STAGE_SWEEP_INTERVAL_MS } from "./native-profile-stage-store";
|
|
22
22
|
import type { NativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
23
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
23
24
|
|
|
24
25
|
export type NativeMainStartupGateSnapshot =
|
|
25
26
|
| { status: "ready"; homeId: string | null }
|
|
@@ -701,7 +702,9 @@ export async function releaseNativeMainStartupLifecycle(server: object): Promise
|
|
|
701
702
|
|
|
702
703
|
export function isNativeMainTrafficBlocked(): boolean {
|
|
703
704
|
const reason = activeServiceOwnershipBlockReason();
|
|
704
|
-
|
|
705
|
+
// A passive read (a Remote Link Child's, #6959) reports the gate as it stands: re-inspecting
|
|
706
|
+
// service ownership can activate the native-main lifecycle and its recovery on this machine.
|
|
707
|
+
if (reason !== null && !isPassiveRead() && reprobeServiceOwnership(reason)) {
|
|
705
708
|
// The host became ownable after boot (#2108): the fence lifts here rather than
|
|
706
709
|
// waiting for the restart the reporter had to perform by hand.
|
|
707
710
|
return activeServiceOwnershipBlockReason() !== null || snapshot.status === "blocked";
|
|
@@ -318,7 +318,7 @@ export function recoverIfNeeded(
|
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
try {
|
|
321
|
-
publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, hooks));
|
|
321
|
+
publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, expectingImage(hooks, record.postConfig)));
|
|
322
322
|
} catch (error) {
|
|
323
323
|
return {
|
|
324
324
|
ok: false,
|
|
@@ -336,7 +336,7 @@ export function recoverIfNeeded(
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
try {
|
|
339
|
-
publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, hooks));
|
|
339
|
+
publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, expectingImage(hooks, record.postStore)));
|
|
340
340
|
} catch (error) {
|
|
341
341
|
return {
|
|
342
342
|
ok: false,
|
|
@@ -363,3 +363,21 @@ function restore(path: string, bytes: string | null, hooks: AtomicWriteHooks): v
|
|
|
363
363
|
if (bytes === null) durableDelete(path, hooks);
|
|
364
364
|
else durableWrite(path, bytes, hooks);
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Bind a write to the image the caller classified. The rename retry hooks check
|
|
369
|
+
* path and inode identity only; an in-place edit by another process during a
|
|
370
|
+
* Windows sharing-violation retry keeps the inode, so without this re-read a
|
|
371
|
+
* later attempt would replace newer bytes. Checked before every attempt.
|
|
372
|
+
*/
|
|
373
|
+
export function expectingImage(hooks: AtomicWriteHooks, expectedHash: string): AtomicWriteHooks {
|
|
374
|
+
return {
|
|
375
|
+
...hooks,
|
|
376
|
+
validateBeforeRename: (targetPath: string) => {
|
|
377
|
+
hooks.validateBeforeRename?.(targetPath);
|
|
378
|
+
if (hashBytes(readOrNull(targetPath)) !== expectedHash) {
|
|
379
|
+
throw new Error(targetPath + " changed during the write; refusing to overwrite it. Files and recovery evidence were preserved.");
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -119,3 +119,52 @@ export function decodeTomlBasicString(literal: string): string | null {
|
|
|
119
119
|
}
|
|
120
120
|
return out;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/** Shared scalar assignment matching; quoted and escaped keys retain their spelling. */
|
|
124
|
+
export function matchKeyHead(line: string, key: string): { prefix: string; rest: string } | null {
|
|
125
|
+
const m = /^\s*("(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*/.exec(line);
|
|
126
|
+
if (!m) return null;
|
|
127
|
+
const token = m[1]!;
|
|
128
|
+
const decoded = token.startsWith('"') ? decodeTomlBasicString(token)
|
|
129
|
+
: token.startsWith("'") ? token.slice(1, -1) : token;
|
|
130
|
+
return decoded === key ? { prefix: m[0], rest: line.slice(m[0].length) } : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* For each line, whether it begins outside every string and composite span, or
|
|
135
|
+
* `null` when a span never closes and the scope of later lines is unknown.
|
|
136
|
+
*
|
|
137
|
+
* Line matchers for assignments and table headers must only look at lines that
|
|
138
|
+
* start at top level: `"model_instructions_file" = "x"` written inside a
|
|
139
|
+
* `"""…"""` value or a multi-line array is prose, not syntax. Readers and
|
|
140
|
+
* editors share this so a write and the snapshot read back agree.
|
|
141
|
+
*/
|
|
142
|
+
export function lexicalLineStarts(lines: readonly string[]): boolean[] | null {
|
|
143
|
+
const starts: boolean[] = [];
|
|
144
|
+
let open: string | null = null;
|
|
145
|
+
let depth = 0;
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
starts.push(open === null && depth === 0);
|
|
148
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
149
|
+
const char = line[i]!;
|
|
150
|
+
if (open !== null) {
|
|
151
|
+
if (char === "\\" && (open === '"' || open === '"""')) { i += 1; continue; }
|
|
152
|
+
if (char !== open[0]) continue;
|
|
153
|
+
if (open.length === 1) { open = null; continue; }
|
|
154
|
+
let run = 0;
|
|
155
|
+
while (line[i + run] === char) run += 1;
|
|
156
|
+
// Up to two quotes may sit inside the value just before the closing three.
|
|
157
|
+
if (run >= 3) open = null;
|
|
158
|
+
i += run - 1;
|
|
159
|
+
} else if (char === "#") break;
|
|
160
|
+
else if (char === '"' || char === "'") {
|
|
161
|
+
open = line.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
|
|
162
|
+
i += open.length - 1;
|
|
163
|
+
} else if (char === "[" || char === "{") depth += 1;
|
|
164
|
+
else if (char === "]" || char === "}") { depth -= 1; if (depth < 0) return null; }
|
|
165
|
+
}
|
|
166
|
+
// Single-line strings cannot cross a newline.
|
|
167
|
+
if (open !== null && open.length === 1) return null;
|
|
168
|
+
}
|
|
169
|
+
return open === null && depth === 0 ? starts : null;
|
|
170
|
+
}
|