@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.
Files changed (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. 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 storePath = declared.path(args.env, args.home);
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
- const resolvedPath = input.resolvedPaths?.configPath
647
- ?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
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: { configPath: string; detectDir: string };
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 path = realpath(paths.join(directory, platform === "win32" ? "bun.exe" : "bun"));
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" && ((file.mode & 0o022) || (stat(paths.dirname(path)).mode & 0o022))) continue;
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
+ }