@bitkyc08/opencodex 2.84.0 → 2.87.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-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +91 -11
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +16 -3
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +40 -11
- 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/dsh-account.ts +97 -0
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +37 -10
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +44 -3
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +223 -0
- package/src/advisor/context.ts +238 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +353 -0
- package/src/advisor/settings.ts +144 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +24 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +72 -7
- package/src/claude/desktop-3p.ts +75 -19
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +154 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +51 -19
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +165 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +23 -2
- package/src/cli/capabilities-base.ts +39 -11
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +30 -5
- package/src/cli/doctor.ts +15 -1
- package/src/cli/ensure-desktop-cold-start.ts +382 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +4 -2
- package/src/cli/index.ts +14 -5
- package/src/cli/integration-plan-dto.ts +3 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +41 -9
- package/src/cli/restart-scope.ts +3 -1
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +3 -1
- 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 +35 -48
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +81 -7
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +19 -2
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +42 -19
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +35 -86
- package/src/codex/catalog/provider-models.ts +40 -34
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +33 -16
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +63 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/picker-freshness.ts +96 -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 +25 -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 +73 -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 +182 -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 +165 -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/features.ts +85 -45
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account-hard-lock.ts +30 -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 +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +2 -2
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +35 -7
- package/src/config/live-reconcile.ts +51 -2
- package/src/config/load-degrade.ts +23 -1
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/paths.ts +10 -3
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +39 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +39 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +962 -350
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- 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 +2 -0
- package/src/integrations/registry.ts +15 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +72 -0
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +26 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/redact.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +62 -12
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +188 -111
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- 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/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- 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/provider-summary.ts +2 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +5 -0
- package/src/providers/model-rename-migration.ts +7 -5
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +44 -14
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +78 -22
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/responses/state.ts +32 -29
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +254 -221
- package/src/routing/history/ingest.ts +242 -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/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +21 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +15 -0
- package/src/server/chat-completions.ts +82 -10
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +164 -433
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +11 -3
- package/src/server/index/startup-warnings.ts +11 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +14 -11
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +202 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +77 -6
- package/src/server/management/context.ts +4 -0
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +13 -8
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +21 -1
- 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-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +106 -22
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +13 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +48 -19
- package/src/server/responses/adapter-delivery.ts +43 -8
- package/src/server/responses/adapter-dispatch.ts +48 -4
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +368 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +12 -1
- package/src/server/responses/core-combo.ts +141 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +7 -2
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +51 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +25 -6
- package/src/server/responses/core.ts +3 -116
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +19 -11
- package/src/server/responses/passthrough-dispatch.ts +29 -16
- package/src/server/responses/request-prepare.ts +144 -17
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +120 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +166 -38
- package/src/server/responses/sidecar-execution.ts +98 -5
- package/src/server/responses/terminal-guard.ts +4 -17
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +99 -61
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +135 -0
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +124 -6
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +39 -6
- package/src/types/request.ts +38 -2
- package/src/types/tools.ts +2 -0
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -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/npm-invocation.mjs +12 -2
- 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/cost.ts +48 -12
- package/src/usage/expected-prices.ts +15 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
package/src/service/guards.ts
CHANGED
|
@@ -2,14 +2,12 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { getConfigDir, loadConfig } from "../config";
|
|
4
4
|
import { withConfigMutationLockSync } from "../config/mutation-lock";
|
|
5
|
-
import { hardenReusedServiceApiToken,
|
|
5
|
+
import { hardenReusedServiceApiToken, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
6
6
|
import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
9
9
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
10
10
|
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
11
|
-
import { diagnoseService } from "./diagnostics";
|
|
12
|
-
import type { ServiceDiagnostic } from "./diagnostics";
|
|
13
11
|
import { currentCodexHome, currentOpenCodexHome, resolveServiceState, serviceCodexHomeMatchesInstall, servicePathMatchesInstall } from "./state";
|
|
14
12
|
import { resolveCodexSqliteHome } from "../codex/paths";
|
|
15
13
|
import type { CodexHomeDeps } from "../codex/home";
|
|
@@ -76,20 +74,6 @@ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {})
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
/**
|
|
80
|
-
* The `ocx` command a user should rerun for the service state they actually have.
|
|
81
|
-
*
|
|
82
|
-
* `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
|
|
83
|
-
* conflict outright, so recommending repair there names a command guaranteed to fail.
|
|
84
|
-
* Install IS the valid conflict recovery, because `installWindows` removes the native
|
|
85
|
-
* backend first. Exported so the guard tests the real selector rather than a copy of it.
|
|
86
|
-
*/
|
|
87
|
-
export function serviceRetryCommand(
|
|
88
|
-
diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
|
|
89
|
-
): string {
|
|
90
|
-
return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
77
|
/**
|
|
94
78
|
* Refuse a management (admin) token as the data-plane secret.
|
|
95
79
|
*
|
|
@@ -139,50 +123,6 @@ export function assertNotAdminToken(
|
|
|
139
123
|
);
|
|
140
124
|
}
|
|
141
125
|
|
|
142
|
-
/**
|
|
143
|
-
* Preflight for `service install` / `service repair` on the data-plane credential.
|
|
144
|
-
*
|
|
145
|
-
* It used to DEMAND `OPENCODEX_API_AUTH_TOKEN` for a non-loopback hostname, and it threw
|
|
146
|
-
* even when `~/.opencodex/service-api-token` already held a perfectly good token. That is
|
|
147
|
-
* the defect behind the incident this unit exists to close (#4236): an operator exported the
|
|
148
|
-
* ADMIN token as OPENCODEX_API_AUTH_TOKEN because `install` asked for a token, the hub then
|
|
149
|
-
* crash-looped on `assertNotAdminToken`, and `service repair` asked for the same env var
|
|
150
|
-
* again — so the only remembered way to make the command proceed was the thing that broke it.
|
|
151
|
-
*
|
|
152
|
-
* Nobody should have to export a token by hand to run a hub. {@link writeServiceApiTokenFile}
|
|
153
|
-
* provisions one, so the only conditions left that install cannot fix are an admin-token
|
|
154
|
-
* collision in the environment and a token file that exists but cannot be used.
|
|
155
|
-
*/
|
|
156
|
-
export function assertServiceAuthEnvironment(): void {
|
|
157
|
-
const config = loadConfig();
|
|
158
|
-
// Both collision checks come BEFORE the loopback short-circuit, because the launch wrapper
|
|
159
|
-
// exports the token file unconditionally (`buildServiceShellCommand` cats it whenever it
|
|
160
|
-
// exists, whatever the hostname): a management token in either source fences the whole
|
|
161
|
-
// management plane closed at boot, even on a loopback install that needs no admission
|
|
162
|
-
// secret. Returning early is what let that broken state through.
|
|
163
|
-
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
164
|
-
if (present) assertNotAdminToken(present);
|
|
165
|
-
const state = readServiceApiTokenState();
|
|
166
|
-
// An existing FILE holding the admin token is the incident shape itself, and the first round
|
|
167
|
-
// only checked the env var — so install/repair reused it and the hub crash-looped at boot.
|
|
168
|
-
// On a machine connected to a hub this same file holds that hub's issued client key, which
|
|
169
|
-
// is never a management token, so the check is a no-op there.
|
|
170
|
-
if (state.kind === "present") assertNotAdminToken(state.token, process.env, "file");
|
|
171
|
-
if (isLoopbackHostname(config.hostname)) return;
|
|
172
|
-
if (present) return;
|
|
173
|
-
// Absent is fine — install/repair generates one below. `unsafe` is not: the writer refuses
|
|
174
|
-
// to replace a path it cannot vouch for, so say so here, where the operator can still act,
|
|
175
|
-
// instead of failing mid-install. Reached from `service repair` as well as `install`, so
|
|
176
|
-
// name a command that can actually succeed (see serviceRetryCommand).
|
|
177
|
-
if (state.kind !== "unsafe") return;
|
|
178
|
-
const diag = diagnoseService();
|
|
179
|
-
throw new Error(
|
|
180
|
-
`The data-plane token file cannot be used (${state.reason}): ${serviceApiTokenFilePath()}. `
|
|
181
|
-
+ `Move it aside, then rerun \`${serviceRetryCommand(diag)}\`; the service provisions a `
|
|
182
|
-
+ "fresh owner-only token and needs nothing from the environment.",
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
126
|
/** How the data-plane token the service will export was obtained. */
|
|
187
127
|
export type ServiceApiTokenOrigin = "env" | "file" | "generated";
|
|
188
128
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** Linux pidfd ownership for the opt-in sentinel. Loaded only on Linux. */
|
|
2
|
+
import { dlopen, FFIType, read } from "bun:ffi";
|
|
3
|
+
import type { TerminateResult } from "./health-sentinel-main";
|
|
4
|
+
|
|
5
|
+
const ESRCH = 3;
|
|
6
|
+
const ENOSYS = 38;
|
|
7
|
+
// Syscalls from 424 onward share one number on every Linux architecture. Binding is limited to
|
|
8
|
+
// x64/arm64 because the FFI signature below assumes their LP64 integer-register calling convention.
|
|
9
|
+
const SYS_PIDFD_OPEN = 434;
|
|
10
|
+
const SYS_PIDFD_SEND_SIGNAL = 424;
|
|
11
|
+
|
|
12
|
+
type CallResult = { value: number; errno: number };
|
|
13
|
+
export type PidfdSyscalls = {
|
|
14
|
+
open(pid: number): CallResult;
|
|
15
|
+
signal(fd: number, signal: number): CallResult;
|
|
16
|
+
close(fd: number): void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
};
|
|
19
|
+
export type ParentPidfd = { terminate(): TerminateResult; close(): void };
|
|
20
|
+
export type ParentPidfdResult =
|
|
21
|
+
| { kind: "bound"; handle: ParentPidfd }
|
|
22
|
+
| { kind: "unsupported"; reason: string }
|
|
23
|
+
| { kind: "refused"; reason: string };
|
|
24
|
+
|
|
25
|
+
function nativeSyscalls(): PidfdSyscalls {
|
|
26
|
+
const symbols = {
|
|
27
|
+
// syscall is variadic, but these syscalls use integer/pointer arguments only. All supplied
|
|
28
|
+
// slots are machine-word integers (including zero for siginfo and flags), on these LP64 ABIs.
|
|
29
|
+
syscall: { args: [FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64], returns: FFIType.i64 },
|
|
30
|
+
__errno_location: { args: [], returns: FFIType.ptr },
|
|
31
|
+
close: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
32
|
+
} as const;
|
|
33
|
+
const load = (path: string) => dlopen(path, symbols);
|
|
34
|
+
let libc: ReturnType<typeof load>;
|
|
35
|
+
try {
|
|
36
|
+
libc = load("libc.so.6");
|
|
37
|
+
} catch {
|
|
38
|
+
// musl exports libc symbols through its loader; it has no glibc soname or pidfd wrappers.
|
|
39
|
+
libc = load(`/lib/ld-musl-${process.arch === "x64" ? "x86_64" : "aarch64"}.so.1`);
|
|
40
|
+
}
|
|
41
|
+
const call = (nr: number, a: number, b: number, c = 0, d = 0): CallResult => {
|
|
42
|
+
const value = Number(libc.symbols.syscall(nr, a, b, c, d));
|
|
43
|
+
return { value, errno: value === -1 ? read.i32(libc.symbols.__errno_location()!) : 0 };
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
open: pid => call(SYS_PIDFD_OPEN, pid, 0),
|
|
47
|
+
signal: (fd, signal) => call(SYS_PIDFD_SEND_SIGNAL, fd, signal),
|
|
48
|
+
// Never retry close on Linux: even an EINTR has released the descriptor.
|
|
49
|
+
close: fd => { libc.symbols.close(fd); },
|
|
50
|
+
dispose: () => libc.close(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Acquire before arming; reparenting during acquisition invalidates even a successfully opened fd. */
|
|
55
|
+
export function openParentPidfd(
|
|
56
|
+
pid: number,
|
|
57
|
+
isParent: () => boolean,
|
|
58
|
+
options: { arch?: string; load?: () => PidfdSyscalls } = {},
|
|
59
|
+
): ParentPidfdResult {
|
|
60
|
+
if (!isParent()) return { kind: "refused", reason: "the sentinel is not the watched process's child" };
|
|
61
|
+
const arch = options.arch ?? process.arch;
|
|
62
|
+
if (arch !== "x64" && arch !== "arm64") return { kind: "unsupported", reason: `pidfd syscall ABI is unsupported on ${arch}` };
|
|
63
|
+
let api: PidfdSyscalls;
|
|
64
|
+
try { api = (options.load ?? nativeSyscalls)(); }
|
|
65
|
+
catch { return { kind: "refused", reason: "Linux pidfd libc binding is unavailable" }; }
|
|
66
|
+
let fd: number | null = null;
|
|
67
|
+
let closed = false;
|
|
68
|
+
let signalled = false;
|
|
69
|
+
const close = () => {
|
|
70
|
+
if (closed) return;
|
|
71
|
+
closed = true;
|
|
72
|
+
try { if (fd !== null) api.close(fd); } finally { api.dispose(); }
|
|
73
|
+
};
|
|
74
|
+
const refuse = (reason: string): ParentPidfdResult => { close(); return { kind: "refused", reason }; };
|
|
75
|
+
try {
|
|
76
|
+
const opened = api.open(pid);
|
|
77
|
+
if (opened.value < 0) {
|
|
78
|
+
close();
|
|
79
|
+
return opened.errno === ENOSYS
|
|
80
|
+
? { kind: "unsupported", reason: "pidfd_open is unsupported by this kernel" }
|
|
81
|
+
: { kind: "refused", reason: `pidfd_open failed (errno ${opened.errno})` };
|
|
82
|
+
}
|
|
83
|
+
fd = opened.value;
|
|
84
|
+
if (!isParent()) return refuse("the watched parent exited during pidfd acquisition");
|
|
85
|
+
// Check both syscall support and permission before arming, without sending a signal.
|
|
86
|
+
const probe = api.signal(fd, 0);
|
|
87
|
+
if (probe.value < 0) {
|
|
88
|
+
close();
|
|
89
|
+
return probe.errno === ENOSYS
|
|
90
|
+
? { kind: "unsupported", reason: "pidfd_send_signal is unsupported by this kernel" }
|
|
91
|
+
: { kind: "refused", reason: `pidfd signal check failed (errno ${probe.errno})` };
|
|
92
|
+
}
|
|
93
|
+
if (!isParent()) return refuse("the watched parent exited during pidfd verification");
|
|
94
|
+
return {
|
|
95
|
+
kind: "bound",
|
|
96
|
+
handle: {
|
|
97
|
+
close,
|
|
98
|
+
terminate: () => {
|
|
99
|
+
if (closed || signalled) return "not_target";
|
|
100
|
+
// A throwing binding must settle the recorded intent as "failed", like process.kill does.
|
|
101
|
+
let sent: CallResult;
|
|
102
|
+
try { sent = api.signal(fd!, 9); } catch { return "failed"; }
|
|
103
|
+
if (sent.value < 0) return sent.errno === ESRCH ? "not_target" : "failed";
|
|
104
|
+
signalled = true;
|
|
105
|
+
return "terminated";
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return refuse("Linux pidfd binding failed");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* - Windows reuses ids quickly, so the sentinel opens a handle on its parent at startup, accepts it
|
|
13
13
|
* only if that process was created before the sentinel, and terminates through the handle
|
|
14
14
|
* (health-sentinel-windows.ts). While the handle is open the id cannot be reused.
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* residual is accepted and documented in structure/ops/service-health-recovery.md.
|
|
15
|
+
* - Linux holds a pidfd after rechecking the live parent relationship, and signals that fd.
|
|
16
|
+
* Only unsupported kernels/architectures retain the live-ppid fallback used by macOS.
|
|
17
|
+
* - macOS checks the live parent id immediately before SIGKILL; PID reuse between those two
|
|
18
|
+
* syscalls remains a documented residual (structure/ops/service-health-recovery.md).
|
|
20
19
|
*
|
|
21
20
|
* SIGKILL rather than SIGTERM: a proxy whose event loop is stuck cannot run its own SIGTERM
|
|
22
21
|
* handler, and systemd counts a SIGTERM death as a clean exit that `Restart=on-failure` does not
|
|
@@ -38,7 +37,7 @@ import {
|
|
|
38
37
|
type HealthProbeOutcome,
|
|
39
38
|
type HealthSentinelTiming,
|
|
40
39
|
} from "./health-sentinel-policy";
|
|
41
|
-
import { probeHealth, type HealthProbeTarget } from "./health-sentinel-probe";
|
|
40
|
+
import { probeHealth, probeUrlHost, type HealthProbeTarget } from "./health-sentinel-probe";
|
|
42
41
|
|
|
43
42
|
export const HEALTH_SENTINEL_ENV = {
|
|
44
43
|
targetPid: "OCX_HEALTH_SENTINEL_TARGET_PID",
|
|
@@ -80,6 +79,8 @@ export type HealthSentinelDeps = {
|
|
|
80
79
|
isTargetGone(): boolean;
|
|
81
80
|
/** Bind to the target once at startup; a failure stands the sentinel down. */
|
|
82
81
|
bindTarget(): { ok: true } | { ok: false; reason: string };
|
|
82
|
+
/** Release the process owner on every exit, refusal, cancellation or exception. */
|
|
83
|
+
closeTarget(): void;
|
|
83
84
|
/** Terminate the bound target, re-proving it is still the target at the last moment. */
|
|
84
85
|
terminateTarget(): TerminateResult;
|
|
85
86
|
/** Run under the ownership mutation lease, released on return; "busy" when it could not be taken promptly. */
|
|
@@ -143,7 +144,17 @@ type Attempt =
|
|
|
143
144
|
| { kind: "gone" }
|
|
144
145
|
| { kind: "intent"; entry: RecoveryEntry };
|
|
145
146
|
|
|
147
|
+
function matchesProbeTarget(runtime: SentinelRuntimeRecord | null, pid: number, target: HealthProbeTarget): boolean {
|
|
148
|
+
return runtime !== null && runtime.pid === pid && !runtime.sibling
|
|
149
|
+
&& runtime.port === target.port && probeUrlHost(runtime.hostname) === target.hostname;
|
|
150
|
+
}
|
|
151
|
+
|
|
146
152
|
export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<HealthSentinelResult> {
|
|
153
|
+
try { return await observeHealthSentinel(deps); }
|
|
154
|
+
finally { deps.closeTarget(); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function observeHealthSentinel(deps: HealthSentinelDeps): Promise<HealthSentinelResult> {
|
|
147
158
|
const timing: HealthSentinelTiming = { ...DEFAULT_HEALTH_SENTINEL_TIMING, ...deps.timing };
|
|
148
159
|
const standDown = (reason: string): HealthSentinelResult => {
|
|
149
160
|
deps.log(`standing down: ${reason} (pid ${deps.targetPid})`);
|
|
@@ -162,12 +173,18 @@ export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<Healt
|
|
|
162
173
|
recoveryHistory: history,
|
|
163
174
|
timing,
|
|
164
175
|
});
|
|
176
|
+
let pinnedTarget: HealthProbeTarget | null = null;
|
|
165
177
|
for (;;) {
|
|
166
178
|
if (deps.isTargetGone()) return { outcome: "target_exited" };
|
|
167
179
|
const runtime = deps.readRuntime();
|
|
168
180
|
if (!runtime || runtime.pid !== deps.targetPid) return standDown("the runtime record no longer names the watched process");
|
|
169
181
|
if (runtime.sibling) return standDown("the watched process is a sibling proxy");
|
|
170
|
-
const
|
|
182
|
+
const hostname = probeUrlHost(runtime.hostname);
|
|
183
|
+
if (hostname === null) return standDown("the recorded bind address is not numeric; DNS names cannot authorize recovery");
|
|
184
|
+
const target: HealthProbeTarget = pinnedTarget ?? { hostname, port: runtime.port };
|
|
185
|
+
if (!matchesProbeTarget(runtime, deps.targetPid, target)) return standDown("the recorded probe endpoint changed");
|
|
186
|
+
pinnedTarget = target;
|
|
187
|
+
const outcome = await raceGone(deps.probe(target, timing.probeTimeoutMs), exitSignal);
|
|
171
188
|
if (outcome === GONE) return { outcome: "target_exited" };
|
|
172
189
|
const wasArmed = policy.state().armed;
|
|
173
190
|
const decision = policy.observe(outcome);
|
|
@@ -178,7 +195,7 @@ export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<Healt
|
|
|
178
195
|
// Everything that could have changed during the probe is re-read under the lease.
|
|
179
196
|
if (deps.isTargetGone()) return { kind: "gone" };
|
|
180
197
|
const current = deps.readRuntime();
|
|
181
|
-
if (!current
|
|
198
|
+
if (!matchesProbeTarget(current, deps.targetPid, target)) {
|
|
182
199
|
return { kind: "stand_down", reason: "the runtime record changed while the target was unresponsive" };
|
|
183
200
|
}
|
|
184
201
|
// Persist first: an attempted recovery must count toward the cap, and on Windows this
|
|
@@ -202,7 +219,10 @@ export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<Healt
|
|
|
202
219
|
const { entry } = attempt;
|
|
203
220
|
// The lease, history and log work above takes time. Re-check that the evidence is still
|
|
204
221
|
// fresh right before the irreversible step: a proxy that recovered meanwhile must not be
|
|
205
|
-
// killed on old observations. A clock that ran backwards makes the evidence stale too.
|
|
222
|
+
// killed on old observations. A clock that ran backwards makes the evidence stale too. The
|
|
223
|
+
// final runtime-record read is included before the freshness check so time spent on it
|
|
224
|
+
// cannot authorize termination on stale observations.
|
|
225
|
+
const finalRuntime = deps.readRuntime();
|
|
206
226
|
const evidenceAge = deps.now() - outcome.endedAt;
|
|
207
227
|
if (evidenceAge < 0 || evidenceAge > timing.maxEvidenceAgeMs || deps.isTargetGone()) {
|
|
208
228
|
settle({ ...entry, state: "skipped" });
|
|
@@ -211,6 +231,10 @@ export async function runHealthSentinel(deps: HealthSentinelDeps): Promise<Healt
|
|
|
211
231
|
if (await raceGone(deps.sleep(policy.nextDelayMs()), exitSignal) === GONE) return { outcome: "target_exited" };
|
|
212
232
|
continue;
|
|
213
233
|
}
|
|
234
|
+
if (!matchesProbeTarget(finalRuntime, deps.targetPid, target)) {
|
|
235
|
+
settle({ ...entry, state: "skipped" });
|
|
236
|
+
return standDown("the recorded probe endpoint changed before termination");
|
|
237
|
+
}
|
|
214
238
|
const result = deps.terminateTarget();
|
|
215
239
|
if (result === "not_target") {
|
|
216
240
|
settle({ ...entry, state: "skipped" });
|
|
@@ -329,6 +353,7 @@ export function readSentinelRuntime(path: string): SentinelRuntimeRecord | null
|
|
|
329
353
|
try {
|
|
330
354
|
const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
331
355
|
const { pid, port, hostname } = parsed;
|
|
356
|
+
if (hostname !== undefined && typeof hostname !== "string") return null;
|
|
332
357
|
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
333
358
|
if (typeof port !== "number" || !Number.isSafeInteger(port) || port <= 0 || port > 65_535) return null;
|
|
334
359
|
return { pid, port, ...(typeof hostname === "string" ? { hostname } : {}), sibling: parsed.siblingOfPort !== undefined };
|
|
@@ -388,59 +413,72 @@ export async function runHealthSentinelProcess(env: NodeJS.ProcessEnv = process.
|
|
|
388
413
|
if (opened.ok) windowsHandle = opened.handle;
|
|
389
414
|
else windowsBindFailure = opened.reason;
|
|
390
415
|
}
|
|
416
|
+
let linuxHandle: import("./health-sentinel-linux").ParentPidfd | null = null;
|
|
417
|
+
let linuxBindFailure: string | null = null;
|
|
418
|
+
if (process.platform === "linux") {
|
|
419
|
+
const { openParentPidfd } = await import("./health-sentinel-linux");
|
|
420
|
+
const opened = openParentPidfd(targetPid, () => !gone && process.ppid === targetPid);
|
|
421
|
+
if (opened.kind === "bound") linuxHandle = opened.handle;
|
|
422
|
+
else if (opened.kind === "refused") linuxBindFailure = opened.reason;
|
|
423
|
+
else appendBoundedLog(logPath, `pidfd unavailable: ${opened.reason}; using live parent PID check`);
|
|
424
|
+
}
|
|
391
425
|
const lag = startLagMonitor();
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return "
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
426
|
+
try {
|
|
427
|
+
await runHealthSentinel({
|
|
428
|
+
targetPid,
|
|
429
|
+
now: Date.now,
|
|
430
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
431
|
+
probe: async (target, timeoutMs) => {
|
|
432
|
+
lag.reset();
|
|
433
|
+
const outcome = await probeHealth(target, timeoutMs);
|
|
434
|
+
return outcome.kind === "timeout" ? { ...outcome, sentinelLagMs: lag.read() } : outcome;
|
|
435
|
+
},
|
|
436
|
+
readRuntime: () => readSentinelRuntime(runtimePath),
|
|
437
|
+
targetExited,
|
|
438
|
+
closeTarget: () => { linuxHandle?.close(); windowsHandle?.close(); },
|
|
439
|
+
isTargetGone: () => gone || (windowsHandle?.exited() ?? false),
|
|
440
|
+
bindTarget: () => {
|
|
441
|
+
// POSIX: Bun reads the parent id live, and it stops naming the proxy once the proxy exits.
|
|
442
|
+
// Windows: it is the creating parent's id, so the handle below carries the identity.
|
|
443
|
+
if (process.ppid !== targetPid) return { ok: false, reason: "the sentinel is not the watched process's child" };
|
|
444
|
+
if (linuxBindFailure) return { ok: false, reason: linuxBindFailure };
|
|
445
|
+
if (process.platform !== "win32") return { ok: true };
|
|
446
|
+
return windowsHandle ? { ok: true } : { ok: false, reason: windowsBindFailure ?? "the parent process cannot be opened" };
|
|
447
|
+
},
|
|
448
|
+
terminateTarget: () => {
|
|
449
|
+
if (process.platform === "win32") {
|
|
450
|
+
if (!windowsHandle) return "failed";
|
|
451
|
+
return windowsHandle.terminate(1) ? "terminated" : "failed";
|
|
452
|
+
}
|
|
453
|
+
if (gone || process.ppid !== targetPid) return "not_target";
|
|
454
|
+
if (linuxHandle) return linuxHandle.terminate();
|
|
455
|
+
try {
|
|
456
|
+
process.kill(targetPid, "SIGKILL");
|
|
457
|
+
return "terminated";
|
|
458
|
+
} catch {
|
|
459
|
+
return "failed";
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
withOwnershipLease: run => {
|
|
463
|
+
let lease: { release(): void };
|
|
464
|
+
try {
|
|
465
|
+
lease = acquireOwnershipMutationLease(statePaths, { waitMs: LEASE_WAIT_MS, processImage: () => null });
|
|
466
|
+
} catch {
|
|
467
|
+
return "busy";
|
|
468
|
+
}
|
|
469
|
+
try { return run(); } finally { lease.release(); }
|
|
470
|
+
},
|
|
471
|
+
readHistory: () => {
|
|
472
|
+
const entries = readRecoveryEntries(historyPath);
|
|
473
|
+
return entries === null ? null : countedRecoveryTimes(entries);
|
|
474
|
+
},
|
|
475
|
+
writeHistory: entry => writeRecoveryEntry(historyPath, entry),
|
|
476
|
+
log: line => appendBoundedLog(logPath, linuxHandle && line.startsWith("watching pid ") ? `${line} (pidfd)` : line),
|
|
477
|
+
timing: parseTimingOverride(env[HEALTH_SENTINEL_ENV.timing]),
|
|
478
|
+
});
|
|
479
|
+
} finally {
|
|
480
|
+
lag.stop();
|
|
481
|
+
}
|
|
444
482
|
return 0;
|
|
445
483
|
}
|
|
446
484
|
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* response framing and an absolute deadline. Only the timing decides liveness; the body's `pid`
|
|
8
8
|
* lets the policy tell the target from another process that took the port.
|
|
9
9
|
*/
|
|
10
|
+
import { isIP } from "node:net";
|
|
11
|
+
|
|
12
|
+
import { isWildcardHostname } from "../codex/loopback-target";
|
|
10
13
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
11
14
|
import type { HealthProbeOutcome } from "./health-sentinel-policy";
|
|
12
15
|
|
|
@@ -17,12 +20,24 @@ export type HealthProbeIo = {
|
|
|
17
20
|
fetch?: (url: string, timeoutMs: number) => Promise<Response>;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Numeric probe endpoint only; unknown DNS destinations cannot authorize recovery. Wildcard
|
|
25
|
+
* spellings use the server's own predicate (`*`, `0`, `00.0.0.000`, `::0`, ...), so every bind
|
|
26
|
+
* the server accepts as a wildcard is probed on loopback instead of standing the sentinel down.
|
|
27
|
+
*/
|
|
28
|
+
export function probeUrlHost(hostname: string | undefined): string | null {
|
|
29
|
+
let value = (hostname ?? "").trim();
|
|
30
|
+
if (isWildcardHostname(value)) return value.includes(":") ? "[::1]" : "127.0.0.1";
|
|
31
|
+
if (value.startsWith("[") || value.endsWith("]")) {
|
|
32
|
+
if (!value.startsWith("[") || !value.endsWith("]")) return null;
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
if (isIP(value) !== 6) return null;
|
|
35
|
+
}
|
|
36
|
+
if (!value || value === "0.0.0.0" || /^localhost\.?$/i.test(value)) return "127.0.0.1";
|
|
37
|
+
if (isIP(value) === 4) return value;
|
|
38
|
+
if (isIP(value) !== 6 || value.includes("%")) return null;
|
|
39
|
+
const host = new URL(`http://[${value}]/`).hostname;
|
|
40
|
+
return host === "[::]" ? "[::1]" : host;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/** `pid` from a `/healthz` JSON body, or null when the body does not carry one. */
|
|
@@ -39,8 +54,10 @@ export async function probeHealth(target: HealthProbeTarget, timeoutMs: number,
|
|
|
39
54
|
const now = io.now ?? Date.now;
|
|
40
55
|
const fetchFn = io.fetch ?? ((url: string, deadlineMs: number) => directLocalHttpFetch(url, { headers: { accept: "application/json" } }, { timeoutMs: deadlineMs }));
|
|
41
56
|
const startedAt = now();
|
|
57
|
+
const hostname = probeUrlHost(target.hostname);
|
|
58
|
+
if (hostname === null) return { kind: "error", startedAt, endedAt: now() };
|
|
42
59
|
try {
|
|
43
|
-
const response = await fetchFn(`http://${
|
|
60
|
+
const response = await fetchFn(`http://${hostname}:${target.port}/healthz`, timeoutMs);
|
|
44
61
|
// The framed body is already in memory; reading it cannot wait on the target.
|
|
45
62
|
const body = await response.text().catch(() => "");
|
|
46
63
|
return { kind: "answered", pid: healthzPid(body), startedAt, endedAt: now() };
|
package/src/service/health.ts
CHANGED
|
@@ -1,77 +1,18 @@
|
|
|
1
|
-
import { win32 } from "node:path";
|
|
2
1
|
import { loadConfig } from "../config";
|
|
3
|
-
import {
|
|
4
|
-
import { PROXY_ENV_KEYS } from "../lib/proxy-env";
|
|
2
|
+
import { resolveServiceListenPort } from "./definition";
|
|
5
3
|
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
6
4
|
import { launchdListenPort } from "./launchd";
|
|
7
5
|
import { serviceLogPath } from "./state";
|
|
8
6
|
import { systemdListenPort } from "./systemd";
|
|
9
7
|
import { windowsListenPort, winswListenPort } from "./windows-ops";
|
|
10
8
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Listen port baked into service wrappers / WinSW XML.
|
|
17
|
-
* Priority: explicit override → OCX_BAKE_PORT (update restart) → config.port → 10100.
|
|
18
|
-
* `config.port === 0` means ephemeral for interactive start; services need a stable pin,
|
|
19
|
-
* so treat 0 / invalid like unset (default 10100) instead of baking `--port 0`.
|
|
20
|
-
*/
|
|
21
|
-
export function resolveServiceListenPort(override?: number): number {
|
|
22
|
-
if (typeof override === "number" && Number.isFinite(override) && override > 0 && override <= 65535) {
|
|
23
|
-
return Math.trunc(override);
|
|
24
|
-
}
|
|
25
|
-
const baked = process.env.OCX_BAKE_PORT?.trim();
|
|
26
|
-
if (baked && /^\d+$/.test(baked)) {
|
|
27
|
-
const n = Number(baked);
|
|
28
|
-
if (n > 0 && n <= 65535) return n;
|
|
29
|
-
}
|
|
30
|
-
const configured = loadConfig().port;
|
|
31
|
-
if (typeof configured === "number" && configured > 0 && configured <= 65535) return configured;
|
|
32
|
-
return 10100;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function buildServiceShellCommand(bun: string, cli: string | null, port = resolveServiceListenPort()): string {
|
|
36
|
-
const tokenFile = serviceApiTokenFilePath();
|
|
37
|
-
const args = cli ? `${shellQuote(cli)} start` : "start";
|
|
38
|
-
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${args} --port ${port}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The same command shape, launched through a stable `ocx` executable instead of an
|
|
43
|
-
* explicit Bun + CLI pair. The token-file preamble is identical and deliberately shared
|
|
44
|
-
* in form: the service still reads the token from disk at start and never carries it in
|
|
45
|
-
* the unit.
|
|
46
|
-
*/
|
|
47
|
-
export function buildServiceLauncherShellCommand(launcher: string, port = resolveServiceListenPort()): string {
|
|
48
|
-
const tokenFile = serviceApiTokenFilePath();
|
|
49
|
-
return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(launcher)} start --port ${port}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Shared tail parser for the baked `--port <n>`.
|
|
54
|
-
*
|
|
55
|
-
* Terminators cover all three artifact shapes: whitespace (batch wrapper, systemd
|
|
56
|
-
* unit), `"` (systemd's quoted ExecStart), `<` (WinSW's `</arguments>`), and `&` (an
|
|
57
|
-
* XML-escaped quote). Matched LAST because every artifact carries the Bun and CLI
|
|
58
|
-
* paths ahead of the argument, and a path containing the literal must not shadow it.
|
|
59
|
-
*/
|
|
60
|
-
export function parseBakedListenPort(read: () => string): number | null {
|
|
61
|
-
try {
|
|
62
|
-
const last = [...read().matchAll(/start --port (\d{1,5})(?:\s|"|&|<|$)/gm)].at(-1);
|
|
63
|
-
if (!last) return null;
|
|
64
|
-
const n = Number(last[1]);
|
|
65
|
-
return n > 0 && n <= 65535 ? n : null;
|
|
66
|
-
} catch {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
9
|
+
export { resolveServiceListenPort };
|
|
10
|
+
export { shellQuote, buildServiceShellCommand, buildServiceLauncherShellCommand, parseBakedListenPort, resolvedProxyEnv } from "./definition";
|
|
70
11
|
|
|
71
12
|
/**
|
|
72
13
|
* The listen port of the INSTALLED service artifact, falling back to the configured
|
|
73
|
-
* one.
|
|
74
|
-
*
|
|
14
|
+
* one. This historical artifact chain can read retained files from another platform;
|
|
15
|
+
* platform-specific launchd command checks instead use only the plist and configured port.
|
|
75
16
|
*/
|
|
76
17
|
export function installedServiceListenPort(): number {
|
|
77
18
|
return launchdListenPort()
|
|
@@ -239,28 +180,3 @@ export async function reportServiceServing(
|
|
|
239
180
|
export function serviceRepairCommand(): string {
|
|
240
181
|
return "ocx service repair";
|
|
241
182
|
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Outbound proxy settings the installing shell had, resolved for baking into a service
|
|
245
|
-
* definition.
|
|
246
|
-
*
|
|
247
|
-
* A service manager does not inherit the environment of the shell that installed it, and
|
|
248
|
-
* `ExecStart=/bin/sh -lc` is dash on Ubuntu/WSL — login dash reads `.profile`, not
|
|
249
|
-
* `.bashrc`, which is where proxy exports usually live. So a user who needs a proxy to
|
|
250
|
-
* reach the upstream got a service that dialed direct: the socket was reset, the retry
|
|
251
|
-
* budget drained, and the request surfaced as `502 Provider unreachable` (#2107). The
|
|
252
|
-
* same install driven through `ocx codex-shim` worked, because that path spawns with
|
|
253
|
-
* `{ ...process.env }`.
|
|
254
|
-
*
|
|
255
|
-
* Lower-case variants are honored because curl-style tooling sets them and the runtime's
|
|
256
|
-
* own `applyProxyEnv` already treats both cases as equivalent. Only the canonical
|
|
257
|
-
* upper-case name is baked, so a definition never carries two spellings of one setting.
|
|
258
|
-
*/
|
|
259
|
-
export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
|
|
260
|
-
const resolved: { name: string; value: string }[] = [];
|
|
261
|
-
for (const key of PROXY_ENV_KEYS) {
|
|
262
|
-
const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
|
|
263
|
-
if (value) resolved.push({ name: key, value });
|
|
264
|
-
}
|
|
265
|
-
return resolved;
|
|
266
|
-
}
|