@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
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
captureSnapshot,
|
|
17
17
|
clearPruneFailure,
|
|
18
18
|
countSnapshots,
|
|
19
|
+
discardUncommittedSnapshot,
|
|
19
20
|
findOperation,
|
|
20
21
|
findCommittedOperation,
|
|
21
22
|
listOperations,
|
|
@@ -53,6 +54,7 @@ export interface IntegrationStateStore {
|
|
|
53
54
|
findOperation(opId: string): JournalEntry | null;
|
|
54
55
|
findCommittedOperation(opId: string): JournalEntry | null;
|
|
55
56
|
captureSnapshot(clientId: IntegrationClientId, opId: string, text: string | null): SnapshotRef;
|
|
57
|
+
discardUncommittedSnapshot(clientId: IntegrationClientId, opId: string): void;
|
|
56
58
|
readSnapshot(entry: JournalEntry):
|
|
57
59
|
| { kind: "none" }
|
|
58
60
|
| { kind: "stored"; text: string; path: string }
|
|
@@ -92,6 +94,7 @@ export function createIntegrationStateStore(root: string = integrationsDir()): I
|
|
|
92
94
|
findOperation: opId => findOperation(opId, dir),
|
|
93
95
|
findCommittedOperation: opId => findCommittedOperation(opId, dir),
|
|
94
96
|
captureSnapshot: (clientId, opId, text) => captureSnapshot(clientId, opId, text, dir),
|
|
97
|
+
discardUncommittedSnapshot: (clientId, opId) => discardUncommittedSnapshot(clientId, opId, dir),
|
|
95
98
|
readSnapshot: entry => readSnapshot(entry, dir),
|
|
96
99
|
countSnapshots: clientId => countSnapshots(clientId, dir),
|
|
97
100
|
pruneSnapshots: clientId => pruneSnapshots(clientId, dir),
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* nesting into a file holding the user's other providers would trade a
|
|
24
24
|
* silent no-op for a silent loss.
|
|
25
25
|
*/
|
|
26
|
-
import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
26
|
+
import { EXPORT_CLIENTS, ClientPathError, zcodeLegacyHomeDir, zcodeProviderStorePath, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
27
27
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
28
28
|
import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
|
|
29
29
|
import type { OwnershipRecord } from "./ownership";
|
|
30
|
-
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
30
|
+
import { INTEGRATION_CLIENTS, resolveIntegrationPaths, type ResolvedIntegrationPaths, type IntegrationClientId } from "./registry";
|
|
31
31
|
|
|
32
32
|
export interface IntegrationTarget {
|
|
33
33
|
/** The file this operation reads, writes, journals and records ownership for. */
|
|
@@ -51,6 +51,8 @@ export interface IntegrationTarget {
|
|
|
51
51
|
* to is as effective as it ever was.
|
|
52
52
|
*/
|
|
53
53
|
readonly ineffective: IneffectiveWrite | null;
|
|
54
|
+
/** Active root unknown; the named config is only a historical removal target. */
|
|
55
|
+
readonly pathError?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface IneffectiveWrite {
|
|
@@ -153,12 +155,44 @@ export function resolveIntegrationTarget(args: {
|
|
|
153
155
|
record: OwnershipRecord | null;
|
|
154
156
|
env?: NodeJS.ProcessEnv;
|
|
155
157
|
home?: string;
|
|
158
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
156
159
|
}): IntegrationTarget {
|
|
157
160
|
const { clientId, configPath, io, record } = args;
|
|
158
161
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
159
162
|
if (!declared) return configFileTarget(clientId, configPath, null);
|
|
160
|
-
const
|
|
163
|
+
const paths = clientId === "zcode"
|
|
164
|
+
? args.resolvedPaths ?? resolveIntegrationPaths(clientId, args.env, args.home, undefined, io)
|
|
165
|
+
: undefined;
|
|
166
|
+
const storePath = paths?.storePath ?? declared.path(args.env, args.home, io);
|
|
161
167
|
const kind = io.statKind(storePath);
|
|
168
|
+
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
169
|
+
const historicalStore = clientId === "zcode"
|
|
170
|
+
? zcodeProviderStorePath({ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, io)
|
|
171
|
+
: null;
|
|
172
|
+
if (clientId === "zcode" && owned && owned.configPath !== configPath && owned.configPath !== storePath
|
|
173
|
+
&& owned.configPath !== historicalStore
|
|
174
|
+
&& INTEGRATION_CLIENTS[clientId].bindsDriftedRecord?.(owned.configPath, args.env, args.home) !== true
|
|
175
|
+
&& io.statKind(owned.configPath) !== "missing") {
|
|
176
|
+
throw new ClientPathError(`ZCode ownership remains at ${owned.configPath}. Its location cannot be bound to the current home or selectors; restore the original selectors and disable it before enabling a different data root. Ownership and recovery evidence are retained.`);
|
|
177
|
+
}
|
|
178
|
+
// An explicit provider-file selector can establish the active target even
|
|
179
|
+
// when the bootstrap is unreadable, but absence cannot justify a legacy write.
|
|
180
|
+
if (paths?.pathError && (!(args.env ?? process.env).ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim() || kind !== "file")) {
|
|
181
|
+
const removal = owned?.configPath === historicalStore
|
|
182
|
+
? storeTarget(declared, historicalStore)
|
|
183
|
+
: configFileTarget(clientId, configPath, null);
|
|
184
|
+
return { ...removal, pathError: paths.pathError };
|
|
185
|
+
}
|
|
186
|
+
if (historicalStore && owned?.configPath === historicalStore && historicalStore !== storePath
|
|
187
|
+
&& recordedBlockStillPresent(io, declared.format, historicalStore, owned)) {
|
|
188
|
+
return storeTarget(declared, historicalStore, { store: storePath, why: "owned-config-file" });
|
|
189
|
+
}
|
|
190
|
+
// Relocation must not orphan a block at the default historical path, even
|
|
191
|
+
// when the relocated store has not been created yet.
|
|
192
|
+
if (clientId === "zcode" && owned?.configPath === configPath && configPath !== paths?.configPath
|
|
193
|
+
&& recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)) {
|
|
194
|
+
return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
|
|
195
|
+
}
|
|
162
196
|
if (kind === "missing") {
|
|
163
197
|
const missing = declared.missingStore;
|
|
164
198
|
return missing?.readsStore(storePath, path => io.statKind(path)) === true
|
|
@@ -182,7 +216,6 @@ export function resolveIntegrationTarget(args: {
|
|
|
182
216
|
* file holds it. Disable has to remove what we wrote from where we wrote it,
|
|
183
217
|
* and an apply must never leave a block in one file while writing another.
|
|
184
218
|
*/
|
|
185
|
-
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
186
219
|
if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
|
|
187
220
|
return storeTarget(declared, storePath, established ? null : unestablished);
|
|
188
221
|
}
|
|
@@ -214,6 +247,7 @@ export function declaredIntegrationTarget(args: {
|
|
|
214
247
|
resolvedConfigPath: string;
|
|
215
248
|
env?: NodeJS.ProcessEnv;
|
|
216
249
|
home?: string;
|
|
250
|
+
io?: Pick<IntegrationIO, "readText">;
|
|
217
251
|
}): IntegrationTarget | null {
|
|
218
252
|
const { clientId, configPath, resolvedConfigPath } = args;
|
|
219
253
|
if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
|
|
@@ -222,8 +256,11 @@ export function declaredIntegrationTarget(args: {
|
|
|
222
256
|
}
|
|
223
257
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
224
258
|
if (!declared) return null;
|
|
259
|
+
if (clientId === "zcode" && configPath === zcodeProviderStorePath(
|
|
260
|
+
{ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, args.io,
|
|
261
|
+
)) return storeTarget(declared, configPath);
|
|
225
262
|
try {
|
|
226
|
-
if (declared.path(args.env, args.home) !== configPath) return null;
|
|
263
|
+
if (declared.path(args.env, args.home, args.io) !== configPath) return null;
|
|
227
264
|
} catch (error) {
|
|
228
265
|
// A store the operator relocated with a path we cannot resolve proves
|
|
229
266
|
// nothing about the row, so the row is not a legal target.
|
|
@@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
|
18
18
|
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
19
|
import { copyPlainData } from "../lib/plain-data";
|
|
20
20
|
import type { OcxConfig } from "../types";
|
|
21
|
-
import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
21
|
+
import { IntegrationWriteSizeError, PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
22
22
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
23
23
|
import {
|
|
24
24
|
fingerprint,
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
40
40
|
isLoopbackOnly,
|
|
41
41
|
resolveIntegrationPaths,
|
|
42
|
+
type ResolvedIntegrationPaths,
|
|
42
43
|
restoreOwnershipCollision,
|
|
43
44
|
type IntegrationClientId,
|
|
44
45
|
} from "./registry";
|
|
@@ -161,6 +162,15 @@ function commit(args: CommitArgs): WriteOutcome {
|
|
|
161
162
|
}
|
|
162
163
|
} catch (error) {
|
|
163
164
|
if (transactionStarted) return compensate(args, error, "could not write both Cline files");
|
|
165
|
+
if (error instanceof IntegrationWriteSizeError && args.snapshotPath) {
|
|
166
|
+
try {
|
|
167
|
+
args.store.discardUncommittedSnapshot(clientId, args.entry.opId);
|
|
168
|
+
} catch (cleanupError) {
|
|
169
|
+
return refuse(clientId, "write_failed", args.state,
|
|
170
|
+
`${messageOf(error)}; could not remove the unused backup: ${messageOf(cleanupError)}`, args.snapshotPath);
|
|
171
|
+
}
|
|
172
|
+
return refuse(clientId, "write_failed", args.state, messageOf(error));
|
|
173
|
+
}
|
|
164
174
|
return refuse(clientId, "write_failed", args.state, messageOf(error), args.snapshotPath);
|
|
165
175
|
}
|
|
166
176
|
try {
|
|
@@ -643,8 +653,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
643
653
|
if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
|
|
644
654
|
|
|
645
655
|
const clientId = entry.clientId;
|
|
646
|
-
|
|
647
|
-
|
|
656
|
+
let resolvedPath: string;
|
|
657
|
+
try {
|
|
658
|
+
resolvedPath = clientId === "zcode" || !input.resolvedPaths
|
|
659
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, undefined, io).configPath
|
|
660
|
+
: input.resolvedPaths.configPath;
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
663
|
+
return refuse(clientId, "unsafe", "unsafe", error.message);
|
|
664
|
+
}
|
|
648
665
|
const configPath = entry.configPath;
|
|
649
666
|
/*
|
|
650
667
|
* Restore acts on the path the operation was journaled against. Resolving a
|
|
@@ -655,7 +672,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
655
672
|
* shape those bytes are in, which is what the state below is measured against.
|
|
656
673
|
*/
|
|
657
674
|
const rowTarget = declaredIntegrationTarget({
|
|
658
|
-
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
|
|
675
|
+
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home, io,
|
|
659
676
|
});
|
|
660
677
|
if (rowTarget === null) {
|
|
661
678
|
return refuse(clientId, "conflict", "conflict",
|
|
@@ -814,7 +831,7 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
|
814
831
|
io: IntegrationIO;
|
|
815
832
|
env: NodeJS.ProcessEnv;
|
|
816
833
|
home: string;
|
|
817
|
-
resolvedPaths:
|
|
834
|
+
resolvedPaths: ResolvedIntegrationPaths;
|
|
818
835
|
};
|
|
819
836
|
|
|
820
837
|
/**
|
|
@@ -854,7 +871,7 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
854
871
|
*/
|
|
855
872
|
const resolvedPaths = input.resolvedPaths
|
|
856
873
|
? { ...input.resolvedPaths }
|
|
857
|
-
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }));
|
|
874
|
+
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }), io);
|
|
858
875
|
/*
|
|
859
876
|
* The configuration and the roster are seams like the others, and they were the two still held
|
|
860
877
|
* by reference. A coordinated write plans from this input, awaits the writer lock and a
|
|
@@ -899,7 +916,7 @@ function storeLockFile(frozen: FrozenIntegrationInput): string | null {
|
|
|
899
916
|
if (!declared?.lockFile) return null;
|
|
900
917
|
let storePath: string;
|
|
901
918
|
try {
|
|
902
|
-
storePath = declared.path(frozen.env, frozen.home);
|
|
919
|
+
storePath = declared.path(frozen.env, frozen.home, frozen.io);
|
|
903
920
|
} catch (error) {
|
|
904
921
|
if (error instanceof ClientPathError) return null;
|
|
905
922
|
throw error;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Host activation seam. Registration is synchronous; optional code loads only on use. */
|
|
2
|
+
import type { OcxConfig } from "../types";
|
|
3
|
+
import { setAdvisorPlanFactory } from "../server/responses/advisor-plan-slot";
|
|
4
|
+
|
|
5
|
+
export function activateAdvisor(config: OcxConfig): void {
|
|
6
|
+
if (config.advisor?.enabled !== true || typeof config.advisor.model !== "string" || !config.advisor.model.trim()) {
|
|
7
|
+
setAdvisorPlanFactory(config, null);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
setAdvisorPlanFactory(config, async input => {
|
|
11
|
+
// A later settings write may disable the same live config before this request starts.
|
|
12
|
+
if (config.advisor?.enabled !== true || !config.advisor.model?.trim()) return null;
|
|
13
|
+
const { createAdvisorRuntimePlan } = await import("../advisor/runtime");
|
|
14
|
+
return createAdvisorRuntimePlan(input);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -169,12 +169,16 @@ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
|
|
|
169
169
|
category: "caches",
|
|
170
170
|
snapshot: usageSummaryRetainedStoreSnapshot,
|
|
171
171
|
evictOldest: evictOldestUsageSummaryForBudget,
|
|
172
|
+
// Deletes one in-memory summary entry.
|
|
173
|
+
passiveEvictable: true,
|
|
172
174
|
},
|
|
173
175
|
{
|
|
174
176
|
id: "usage_snapshot",
|
|
175
177
|
category: "caches",
|
|
176
178
|
snapshot: usageSnapshotRetainedStoreSnapshot,
|
|
177
179
|
evictOldest: evictOldestUsageSnapshot,
|
|
180
|
+
// Drops the retained ledger snapshot or one unpinned aggregate, both in memory.
|
|
181
|
+
passiveEvictable: true,
|
|
178
182
|
},
|
|
179
183
|
{
|
|
180
184
|
id: "cursor_blobs",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPassiveRead } from "./passive-read";
|
|
2
|
+
|
|
1
3
|
export type AppOwnedRetainedCategory = "logs" | "caches" | "blobs" | "continuation";
|
|
2
4
|
export type AppOwnedObservedCategory = "translator" | "serialized_tails";
|
|
3
5
|
|
|
@@ -14,6 +16,11 @@ export interface RetainedStoreRegistration {
|
|
|
14
16
|
category: AppOwnedRetainedCategory;
|
|
15
17
|
snapshot(): RetainedStoreSnapshot;
|
|
16
18
|
evictOldest(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Eviction only releases memory: no spill, snapshot or other write. Only such stores are
|
|
21
|
+
* demoted inside a passive read (#6959); the rest wait for this process's own next enforcement.
|
|
22
|
+
*/
|
|
23
|
+
passiveEvictable?: true;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export interface ObservedBufferRegistration {
|
|
@@ -168,11 +175,15 @@ function nextCandidate(
|
|
|
168
175
|
snapshots: ReadonlyMap<string, RetainedStoreSnapshot>,
|
|
169
176
|
ineligible: Set<string>,
|
|
170
177
|
): RetainedStoreRegistration | undefined {
|
|
178
|
+
// A passive read (a Remote Link Child's, #6959) demotes only stores whose eviction is memory-only:
|
|
179
|
+
// a continuation spills to disk and a replay cache rewrites its snapshot.
|
|
180
|
+
const passive = isPassiveRead();
|
|
171
181
|
for (const category of CATEGORY_ORDER) {
|
|
172
182
|
let candidate: RetainedStoreRegistration | undefined;
|
|
173
183
|
let candidateAt = Number.POSITIVE_INFINITY;
|
|
174
184
|
for (const registration of retainedStores.values()) {
|
|
175
185
|
if (registration.category !== category || ineligible.has(registration.id)) continue;
|
|
186
|
+
if (passive && registration.passiveEvictable !== true) continue;
|
|
176
187
|
const snapshot = snapshots.get(registration.id);
|
|
177
188
|
if (!snapshot || snapshot.evictableBytes <= 0) continue;
|
|
178
189
|
const oldestAt = snapshot.oldestAt;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget work the process started and nobody awaits.
|
|
3
|
+
*
|
|
4
|
+
* A lazy quota prime or a startup reconcile is deliberately left running behind a
|
|
5
|
+
* request so the request never waits on it. The work still owns files: the pool
|
|
6
|
+
* prime holds the native-main claim database open across ACL hardening and a WHAM
|
|
7
|
+
* read. Whoever later removes the directory those files live in has to wait for the
|
|
8
|
+
* owner. Without a handle on the work there was nothing to wait for: test teardown
|
|
9
|
+
* spun a synchronous retry loop, which blocks the very event loop the owner needs to
|
|
10
|
+
* reach its `finally` and close the file. On Windows every overlap became a 15 s EBUSY.
|
|
11
|
+
*
|
|
12
|
+
* Registering costs one Set entry per in-flight task and changes nothing about how
|
|
13
|
+
* the work runs. Settling is opt-in: test teardown calls it; requests never do.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface BackgroundWork {
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly settled: Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const inFlight = new Set<BackgroundWork>();
|
|
22
|
+
|
|
23
|
+
/** Record `work` until it settles and return it unchanged. Rejections stay the caller's. */
|
|
24
|
+
export function trackBackgroundWork<T>(label: string, work: Promise<T>): Promise<T> {
|
|
25
|
+
const entry: BackgroundWork = {
|
|
26
|
+
label,
|
|
27
|
+
settled: work.then(() => { inFlight.delete(entry); }, () => { inFlight.delete(entry); }),
|
|
28
|
+
};
|
|
29
|
+
inFlight.add(entry);
|
|
30
|
+
return work;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Labels of work still running, for diagnostics. */
|
|
34
|
+
export function pendingBackgroundWork(): string[] {
|
|
35
|
+
return [...inFlight].map(entry => entry.label);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BackgroundWorkSettlement {
|
|
39
|
+
readonly settled: boolean;
|
|
40
|
+
readonly pending: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wait until no tracked work is in flight, including work started by work that was
|
|
45
|
+
* already running. A bounded wait reports what is still running instead of throwing,
|
|
46
|
+
* so the caller decides whether that is fatal.
|
|
47
|
+
*/
|
|
48
|
+
export async function settleBackgroundWork(options: { timeoutMs?: number } = {}): Promise<BackgroundWorkSettlement> {
|
|
49
|
+
const deadline = options.timeoutMs === undefined ? Infinity : Date.now() + options.timeoutMs;
|
|
50
|
+
while (inFlight.size > 0) {
|
|
51
|
+
const remaining = deadline - Date.now();
|
|
52
|
+
if (remaining <= 0) break;
|
|
53
|
+
const batch = Promise.all([...inFlight].map(entry => entry.settled)).then(() => false);
|
|
54
|
+
if (remaining === Infinity) {
|
|
55
|
+
await batch;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
+
const expired = new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(true), remaining); });
|
|
60
|
+
const timedOut = await Promise.race([batch, expired]);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
if (timedOut) break;
|
|
63
|
+
}
|
|
64
|
+
return { settled: inFlight.size === 0, pending: pendingBackgroundWork() };
|
|
65
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const MAX_BOUNDED_READ_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
5
|
+
|
|
6
|
+
function sameIdentity(
|
|
7
|
+
left: { dev: bigint; ino: bigint },
|
|
8
|
+
right: { dev: bigint; ino: bigint },
|
|
9
|
+
): boolean {
|
|
10
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readBoundedRegularFileSync(path: string, limit: number): Buffer {
|
|
14
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_BOUNDED_READ_BYTES) {
|
|
15
|
+
throw new RangeError("invalid bounded file read limit");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const windows = process.platform === "win32";
|
|
19
|
+
const flags = constants.O_RDONLY | (windows ? 0 : constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
20
|
+
const entryBefore = windows ? lstatSync(path, { bigint: true }) : undefined;
|
|
21
|
+
if (entryBefore && (entryBefore.isSymbolicLink() || !entryBefore.isFile())) {
|
|
22
|
+
throw new Error("not a regular file");
|
|
23
|
+
}
|
|
24
|
+
const fd = openSync(path, flags);
|
|
25
|
+
try {
|
|
26
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
27
|
+
if (!opened.isFile() || opened.size > BigInt(limit)) throw new Error("file is not bounded regular data");
|
|
28
|
+
if (entryBefore && (!sameIdentity(entryBefore, opened) || opened.ino === 0n)) {
|
|
29
|
+
throw new Error("file entry changed while opening");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Start near the observed size, but keep probing up to limit + 1 if the file grows.
|
|
33
|
+
let bytes = Buffer.alloc(Number(opened.size) + 1);
|
|
34
|
+
let length = 0;
|
|
35
|
+
while (length < limit + 1) {
|
|
36
|
+
if (length === bytes.length) {
|
|
37
|
+
const capacity = Math.min(limit + 1, Math.max(bytes.length * 2, bytes.length + READ_CHUNK_BYTES));
|
|
38
|
+
const next = Buffer.alloc(capacity);
|
|
39
|
+
bytes.copy(next, 0, 0, length);
|
|
40
|
+
bytes = next;
|
|
41
|
+
}
|
|
42
|
+
const count = readSync(fd, bytes, length, Math.min(READ_CHUNK_BYTES, bytes.length - length), null);
|
|
43
|
+
if (count === 0) break;
|
|
44
|
+
length += count;
|
|
45
|
+
}
|
|
46
|
+
if (length > limit) throw new Error("file exceeds byte limit");
|
|
47
|
+
|
|
48
|
+
if (entryBefore) {
|
|
49
|
+
const entryAfter = lstatSync(path, { bigint: true });
|
|
50
|
+
if (entryAfter.isSymbolicLink() || !entryAfter.isFile() || !sameIdentity(opened, entryAfter)) {
|
|
51
|
+
throw new Error("file entry changed while reading");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return bytes.subarray(0, length);
|
|
55
|
+
} finally {
|
|
56
|
+
closeSync(fd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -5,7 +5,16 @@ export interface DesktopCliIo {
|
|
|
5
5
|
export interface PathBunIo extends DesktopCliIo {
|
|
6
6
|
now?: () => number;
|
|
7
7
|
realpath?: (path: string) => string;
|
|
8
|
-
stat?: (path: string) => { isFile(): boolean; mode: number };
|
|
8
|
+
stat?: (path: string) => { isFile(): boolean; mode: number; uid?: number };
|
|
9
|
+
lstat?: (path: string) => {
|
|
10
|
+
isFile(): boolean;
|
|
11
|
+
isDirectory(): boolean;
|
|
12
|
+
isSymbolicLink(): boolean;
|
|
13
|
+
mode: number;
|
|
14
|
+
uid?: number;
|
|
15
|
+
};
|
|
16
|
+
readlink?: (path: string) => string;
|
|
17
|
+
geteuid?: () => number | undefined;
|
|
9
18
|
isRealBunBinary?: (path: string) => boolean;
|
|
10
19
|
spawnSync?: (path: string, args: string[], options: {
|
|
11
20
|
env: NodeJS.ProcessEnv;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { posix, win32 } from "node:path";
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
@@ -7,6 +7,7 @@ import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
|
7
7
|
|
|
8
8
|
const STABLE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
9
9
|
const MAX_VERSION_BYTES = 4096;
|
|
10
|
+
const MAX_SYMLINK_EXPANSIONS = 40;
|
|
10
11
|
const IDENTITY_PROBE = "process.stdout.write(String(typeof Bun==='object'&&Bun.version))";
|
|
11
12
|
|
|
12
13
|
function stableVersion(value) {
|
|
@@ -17,6 +18,62 @@ function stableVersion(value) {
|
|
|
17
18
|
return numbers.every(Number.isSafeInteger) ? numbers : null;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function trustedPosixCandidate(candidate, canonical, io) {
|
|
22
|
+
const lstat = io.lstat ?? lstatSync;
|
|
23
|
+
const readlink = io.readlink ?? readlinkSync;
|
|
24
|
+
const uid = io.geteuid?.() ?? process.geteuid?.();
|
|
25
|
+
if (!Number.isSafeInteger(uid) || uid < 0) return false;
|
|
26
|
+
|
|
27
|
+
const walk = path => {
|
|
28
|
+
let pending = path.slice(1).split("/");
|
|
29
|
+
let current = "/";
|
|
30
|
+
let expansions = 0;
|
|
31
|
+
while (pending.length) {
|
|
32
|
+
const [component, ...remaining] = pending;
|
|
33
|
+
pending = remaining;
|
|
34
|
+
if (!component || component === ".") continue;
|
|
35
|
+
if (component === "..") {
|
|
36
|
+
current = posix.dirname(current);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const entry = posix.join(current, component);
|
|
40
|
+
const info = lstat(entry);
|
|
41
|
+
if (!Number.isSafeInteger(info.uid) || (info.uid !== 0 && info.uid !== uid)) return null;
|
|
42
|
+
if (info.isSymbolicLink()) {
|
|
43
|
+
if (++expansions > MAX_SYMLINK_EXPANSIONS) return null;
|
|
44
|
+
const target = readlink(entry);
|
|
45
|
+
pending = target.startsWith("/")
|
|
46
|
+
? [...target.slice(1).split("/"), ...pending]
|
|
47
|
+
: [
|
|
48
|
+
...(current === "/" ? [] : current.slice(1).split("/")),
|
|
49
|
+
...target.split("/"),
|
|
50
|
+
...pending,
|
|
51
|
+
];
|
|
52
|
+
current = "/";
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (remaining.length || pending.length) {
|
|
56
|
+
// No writable-ancestor exemption: a shared sticky directory passes its own
|
|
57
|
+
// ownership check but lets another user replace a child entry between the
|
|
58
|
+
// ancestry walk and the exec probe, substituting an unvalidated binary.
|
|
59
|
+
if (!info.isDirectory() || (info.mode & 0o022) !== 0) return null;
|
|
60
|
+
} else if (!info.isFile() || (info.mode & 0o022) !== 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = entry;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const resolved = walk(candidate);
|
|
69
|
+
if (!resolved || (io.realpath ?? realpathSync.native)(resolved) !== canonical) return false;
|
|
70
|
+
const parent = lstat(posix.dirname(canonical));
|
|
71
|
+
return parent.isDirectory()
|
|
72
|
+
&& Number.isSafeInteger(parent.uid)
|
|
73
|
+
&& (parent.uid === 0 || parent.uid === uid)
|
|
74
|
+
&& (parent.mode & 0o022) === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
20
77
|
/** Discover the first absolute PATH Bun passing version-policy and identity checks. */
|
|
21
78
|
export function findPathBun({
|
|
22
79
|
env = process.env, platform = process.platform, pinnedVersion,
|
|
@@ -54,14 +111,16 @@ export function findPathBun({
|
|
|
54
111
|
if (now() >= end) break;
|
|
55
112
|
if (!directory || !absolute(directory)) continue;
|
|
56
113
|
try {
|
|
57
|
-
const
|
|
114
|
+
const candidatePath = paths.join(directory, platform === "win32" ? "bun.exe" : "bun");
|
|
115
|
+
const path = realpath(candidatePath);
|
|
58
116
|
if (!absolute(path)) continue;
|
|
117
|
+
if (platform !== "win32" && !trustedPosixCandidate(candidatePath, path, io)) continue;
|
|
59
118
|
const identity = platform === "win32" ? path.toLowerCase() : path;
|
|
60
119
|
if (seen.has(identity)) continue;
|
|
61
120
|
seen.add(identity);
|
|
62
121
|
const file = stat(path);
|
|
63
122
|
if (!file.isFile()) continue;
|
|
64
|
-
if (platform !== "win32" && (
|
|
123
|
+
if (platform !== "win32" && (file.mode & 0o022)) continue;
|
|
65
124
|
access(path, constants.X_OK);
|
|
66
125
|
if (!validate(path)) continue;
|
|
67
126
|
const version = probe(path, ["--version"]);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { configuredOutboundFetch, effectiveProxyFor, noProxyMatches, type ProxyEnvMap, type ProxyCapableRequestInit } from "./proxy-env";
|
|
2
|
+
|
|
3
|
+
/** Bind both native desktop transports to one explicit route; never fall back after failure. */
|
|
4
|
+
export function desktopProxyFor(url: URL, env: ProxyEnvMap = process.env): string | false {
|
|
5
|
+
if (noProxyMatches(url, env)) return false;
|
|
6
|
+
const key = url.protocol === "https:" ? "HTTPS_PROXY" : "HTTP_PROXY";
|
|
7
|
+
let selected = effectiveProxyFor(url, env);
|
|
8
|
+
// Unlike an implicit Bun route, this transport explicitly binds HTTP(S) CONNECT.
|
|
9
|
+
// A malformed scheme-specific setting still refuses instead of falling through.
|
|
10
|
+
if (!selected && url.protocol === "https:" && !(env[key]?.trim() || env[key.toLowerCase()]?.trim())) {
|
|
11
|
+
const all = env.ALL_PROXY?.trim() || env.all_proxy?.trim();
|
|
12
|
+
if (all) {
|
|
13
|
+
let parsed: URL; try { parsed = new URL(all); } catch { throw new Error("desktop_egress_proxy_invalid"); }
|
|
14
|
+
if (["http:", "https:"].includes(parsed.protocol)) selected = all;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (selected) {
|
|
18
|
+
const proxy = new URL(selected);
|
|
19
|
+
if (!["http:", "https:", "socks5:", "socks5h:"].includes(proxy.protocol) || !proxy.hostname
|
|
20
|
+
|| proxy.search || proxy.hash || proxy.pathname !== "" && proxy.pathname !== "/") throw new Error("desktop_egress_proxy_invalid");
|
|
21
|
+
return selected;
|
|
22
|
+
}
|
|
23
|
+
// An explicit but unsupported route must not quietly become direct desktop traffic.
|
|
24
|
+
if ([env[key], env[key.toLowerCase()], env.ALL_PROXY, env.all_proxy].some(value => value?.trim())) throw new Error("desktop_egress_proxy_invalid");
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
export function desktopOutboundFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
28
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
29
|
+
return configuredOutboundFetch(input, { ...init, proxy: desktopProxyFor(url) } as ProxyCapableRequestInit);
|
|
30
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { connect as tcpConnect, isIP, type Socket } from "node:net";
|
|
2
|
+
import { checkServerIdentity, connect as tlsConnect, type TLSSocket } from "node:tls";
|
|
3
|
+
import { desktopProxyFor } from "./desktop-proxy-route";
|
|
4
|
+
import { socks5Credentials, socks5Handshake } from "./socks5-handshake";
|
|
5
|
+
|
|
6
|
+
// Shares the SOCKS exchange extracted by lcxhh521 in PR #5947 (efdccdbfac3f).
|
|
7
|
+
// Lifecycle here additionally bounds the entire dial and handles close/abort/header limits.
|
|
8
|
+
export interface DesktopTunnelOptions {
|
|
9
|
+
proxy?: string | false;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/** Isolated test peers; production always uses verified TLS to chatgpt.com:443. */
|
|
13
|
+
target?: { host: string; port: number };
|
|
14
|
+
ca?: string;
|
|
15
|
+
proxyCa?: string;
|
|
16
|
+
}
|
|
17
|
+
const fail = () => new Error("desktop_upstream_connection_failed");
|
|
18
|
+
class HandshakeReader {
|
|
19
|
+
private buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
20
|
+
private failure: Error | null = null;
|
|
21
|
+
private waiting: { ready: () => boolean; resolve: () => void; reject: (error: Error) => void } | null = null;
|
|
22
|
+
private onData = (chunk: Buffer) => {
|
|
23
|
+
if (this.buffer.length + chunk.length > 65536) { this.onFailure(); return; }
|
|
24
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
25
|
+
if (this.waiting?.ready()) { const current = this.waiting; this.waiting = null; current.resolve(); }
|
|
26
|
+
};
|
|
27
|
+
private onFailure = () => { this.failure = fail(); const current = this.waiting; this.waiting = null; current?.reject(this.failure); };
|
|
28
|
+
constructor(private socket: Socket, private signal: AbortSignal) {
|
|
29
|
+
socket.on("data", this.onData); socket.once("error", this.onFailure); socket.once("end", this.onFailure); socket.once("close", this.onFailure);
|
|
30
|
+
signal.addEventListener("abort", this.onFailure, { once: true }); if (signal.aborted) this.onFailure(); socket.resume();
|
|
31
|
+
}
|
|
32
|
+
write(bytes: Uint8Array | string): void { if (this.failure) throw this.failure; this.socket.write(bytes); }
|
|
33
|
+
private async until(ready: () => boolean): Promise<void> {
|
|
34
|
+
if (this.failure) throw this.failure;
|
|
35
|
+
if (ready()) return;
|
|
36
|
+
if (this.waiting) throw fail();
|
|
37
|
+
await new Promise<void>((resolve, reject) => { this.waiting = { ready, resolve, reject }; });
|
|
38
|
+
}
|
|
39
|
+
private take(length: number): Buffer { const out = this.buffer.subarray(0, length); this.buffer = this.buffer.subarray(length); return out; }
|
|
40
|
+
async readExact(length: number): Promise<Buffer> { await this.until(() => this.buffer.length >= length); return this.take(length); }
|
|
41
|
+
async readHead(): Promise<string> { await this.until(() => this.buffer.indexOf("\r\n\r\n") >= 0); return this.take(this.buffer.indexOf("\r\n\r\n") + 4).toString("latin1"); }
|
|
42
|
+
dispose(): void {
|
|
43
|
+
this.socket.pause(); this.socket.removeListener("data", this.onData); this.socket.removeListener("error", this.onFailure);
|
|
44
|
+
this.socket.removeListener("end", this.onFailure); this.socket.removeListener("close", this.onFailure);
|
|
45
|
+
this.signal.removeEventListener("abort", this.onFailure);
|
|
46
|
+
if (this.buffer.length) this.socket.unshift(this.buffer); this.buffer = Buffer.alloc(0); this.onFailure();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function waitConnected(socket: Socket, event: "connect" | "secureConnect", signal: AbortSignal): Promise<void> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const dispose = () => { socket.off(event, done); socket.off("error", failure); socket.off("close", failure); signal.removeEventListener("abort", failure); };
|
|
52
|
+
const done = () => { dispose(); resolve(); };
|
|
53
|
+
const failure = () => { dispose(); reject(fail()); };
|
|
54
|
+
socket.once(event, done); socket.once("error", failure); socket.once("close", failure);
|
|
55
|
+
signal.addEventListener("abort", failure, { once: true }); if (signal.aborted) failure();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Fixed-destination TLS dial for native desktop upgraded sockets. No logs or direct fallback. */
|
|
59
|
+
export async function dialDesktopUpstream(options: DesktopTunnelOptions = {}): Promise<TLSSocket> {
|
|
60
|
+
const timeout = options.timeoutMs ?? 10000;
|
|
61
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 30000) throw fail();
|
|
62
|
+
const cancel = new AbortController(), timer = setTimeout(() => cancel.abort(), timeout);
|
|
63
|
+
const signal = options.signal ? AbortSignal.any([options.signal, cancel.signal]) : cancel.signal;
|
|
64
|
+
const sockets = new Set<Socket>();
|
|
65
|
+
const own = <T extends Socket>(socket: T): T => { sockets.add(socket); socket.on("error", () => {}); return socket; };
|
|
66
|
+
const destroy = () => { for (const socket of sockets) socket.destroy(); };
|
|
67
|
+
signal.addEventListener("abort", destroy, { once: true });
|
|
68
|
+
try {
|
|
69
|
+
if (signal.aborted) throw fail();
|
|
70
|
+
const proxy = options.proxy === undefined ? desktopProxyFor(new URL("https://chatgpt.com")) : options.proxy;
|
|
71
|
+
const target = options.target ?? { host: "chatgpt.com", port: 443 };
|
|
72
|
+
let raw: Socket;
|
|
73
|
+
if (proxy === false) {
|
|
74
|
+
raw = own(tcpConnect(target)); await waitConnected(raw, "connect", signal);
|
|
75
|
+
} else {
|
|
76
|
+
const url = new URL(proxy), protocol = url.protocol;
|
|
77
|
+
if (!["http:", "https:", "socks5:", "socks5h:"].includes(protocol) || !url.hostname || url.search || url.hash || url.pathname !== "" && url.pathname !== "/") throw fail();
|
|
78
|
+
const host = url.hostname.replace(/^\[|\]$/g, ""), port = Number(url.port) || (protocol === "https:" ? 443 : protocol.startsWith("socks") ? 1080 : 80);
|
|
79
|
+
raw = own(tcpConnect({ host, port })); await waitConnected(raw, "connect", signal);
|
|
80
|
+
if (protocol === "https:") {
|
|
81
|
+
raw = own(tlsConnect({ socket: raw, servername: isIP(host) ? undefined : host, ca: options.proxyCa,
|
|
82
|
+
rejectUnauthorized: true, checkServerIdentity: (_name, certificate) => checkServerIdentity(host, certificate), ALPNProtocols: ["http/1.1"] }));
|
|
83
|
+
await waitConnected(raw, "secureConnect", signal);
|
|
84
|
+
}
|
|
85
|
+
const reader = new HandshakeReader(raw, signal);
|
|
86
|
+
try {
|
|
87
|
+
if (protocol.startsWith("socks")) await socks5Handshake(reader, target, socks5Credentials(url));
|
|
88
|
+
else {
|
|
89
|
+
const authority = `${target.host}:${target.port}`;
|
|
90
|
+
const lines = [`CONNECT ${authority} HTTP/1.1`, `Host: ${authority}`];
|
|
91
|
+
if (url.username || url.password) lines.push(`Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`);
|
|
92
|
+
reader.write(lines.join("\r\n") + "\r\n\r\n");
|
|
93
|
+
if (!/^HTTP\/1\.[01] 2\d\d(?: |\r)/.test(await reader.readHead())) throw fail();
|
|
94
|
+
}
|
|
95
|
+
} finally { reader.dispose(); }
|
|
96
|
+
}
|
|
97
|
+
if (signal.aborted || raw.destroyed) throw fail();
|
|
98
|
+
const secured = own(tlsConnect({ socket: raw, servername: "chatgpt.com", ca: options.ca, rejectUnauthorized: true, ALPNProtocols: ["http/1.1"] }));
|
|
99
|
+
// The CONNECT reader paused its stream before handing buffered bytes back. Bun's
|
|
100
|
+
// TLS-over-TLS path needs the outer TLS stream explicitly resumed for the new handshake.
|
|
101
|
+
raw.resume();
|
|
102
|
+
await waitConnected(secured, "secureConnect", signal);
|
|
103
|
+
if (signal.aborted) throw fail();
|
|
104
|
+
return secured;
|
|
105
|
+
} catch { destroy(); throw fail(); }
|
|
106
|
+
finally { clearTimeout(timer); signal.removeEventListener("abort", destroy); }
|
|
107
|
+
}
|