@bitkyc08/opencodex 2.84.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 (246) 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-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -47,7 +47,7 @@ const DATA: Record<string, readonly Row[]> = {
47
47
  "azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]],
48
48
  "cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]],
49
49
  "deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]],
50
- "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-3.8-flash",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]],
50
+ "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-3.8-flash",1048576,65536,"text,image",1],["gemini-4-argon",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]],
51
51
  "minimax": [["MiniMax-M2",196608,128000,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.5",204800,131072,"text",1,null,0.3,1.2,0.03,0.375],["MiniMax-M2.5-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["MiniMax-M2.5-lightning",204800,32000,"text",1,null,0.3,2.4,0,0],["MiniMax-M2.7",204800,131072,"text",1,null,0.3,1.2,0.06,0.375],["MiniMax-M2.7-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["minimax-m3",512000,128000,"text,image",1,null,0.6,2.4,0.12,0],["MiniMax-M3",1000000,128000,"text,image,video",1,null,0.3,1.2,0.06,0],["MiniMax-M3.1-Flash-Preview",1000000,null,"text,image,video",1]],
52
52
  "mistral": [["codestral-latest",256000,4096,"text",0,null,0.3,0.9,0,0],["devstral-2512",262144,262144,"text",0,null,0.4,2,0,0],["devstral-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-medium-2507",128000,128000,"text",0,null,0.4,2,0,0],["devstral-medium-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-small-2505",128000,128000,"text",0,null,0.1,0.3,0,0],["devstral-small-2507",128000,128000,"text",0,null,0.1,0.3,0,0],["labs-devstral-small-2512",256000,256000,"text,image",0,null,0,0,0,0],["magistral-medium-latest",128000,16384,"text",1,null,2,5,0,0],["magistral-small",128000,128000,"text",1,null,0.5,1.5,0,0],["ministral-3b-latest",128000,128000,"text",0,null,0.04,0.04,0,0],["ministral-8b-latest",128000,128000,"text",0,null,0.1,0.1,0,0],["mistral-large-2411",131072,16384,"text",0,null,2,6,0,0],["mistral-large-2512",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-large-latest",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-medium-2505",131072,131072,"text,image",0,null,0.4,2,0,0],["mistral-medium-2508",262144,262144,"text,image",0,null,0.4,2,0,0],["mistral-medium-2604",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-medium-latest",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["mistral-small-2506",128000,16384,"text,image",0,null,0.1,0.3,0,0],["mistral-small-2603",256000,256000,"text,image",1,null,0.15,0.6,0,0],["mistral-small-latest",256000,256000,"text,image",1,null,0.15,0.6,0,0],["open-mistral-7b",8000,8000,"text",0,null,0.25,0.25,0,0],["open-mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["open-mixtral-8x22b",64000,64000,"text",0,null,2,6,0,0],["open-mixtral-8x7b",32000,32000,"text",0,null,0.7,0.7,0,0],["pixtral-12b",128000,128000,"text,image",0,null,0.15,0.15,0,0],["pixtral-large-latest",128000,128000,"text,image",0,null,2,6,0,0]],
53
53
  "moonshot": [["kimi-k2.5",262144,65536,"text,image",1,null,0,0,0,0]],
@@ -12,7 +12,7 @@ import {
12
12
  /** Refresh only previously connected clients; a refused file never blocks its peers. */
13
13
  export async function refreshOwnedCatalogIntegrations(
14
14
  input: Omit<OwnedIntegrationRefreshInput, "clientId">,
15
- clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"],
15
+ clientIds: readonly IntegrationClientId[] = ["pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder"],
16
16
  options: { refreshOnly?: boolean; admit?: () => boolean } = {},
17
17
  ): Promise<OwnedIntegrationRefreshOutcome[]> {
18
18
  // Client files are shared with the live proxy a sibling instance runs beside; their entries
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Design of record: devlog/_fin/260802_client_toggle_api/021 §5-6.
9
9
  */
10
- import { lstatSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
10
+ import { lstatSync, mkdirSync, rmSync, statSync } from "node:fs";
11
11
  import type { ConfigFormat } from "../clients/config-export";
12
+ import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
12
13
  import { canonicalizeJsonc } from "../lib/jsonc";
13
14
  import { MAX_JSON_NESTING } from "./serialize";
14
15
  import { atomicWriteFileNoFollow, isMissingPathError } from "../config/atomic-write";
@@ -24,6 +25,10 @@ import type { IntegrationClientId } from "./registry";
24
25
  * then overwritten.
25
26
  */
26
27
  export const PARSE_FAILED = Symbol("parse-failed");
28
+ const MAX_INTEGRATION_CONFIG_FILE_BYTES = 4 * 1024 * 1024;
29
+
30
+ /** Raised before any target mutation, including pending-marker creation. */
31
+ export class IntegrationWriteSizeError extends Error {}
27
32
 
28
33
  /**
29
34
  * What `JSON.parse` has already discarded by the time we hold the parsed value,
@@ -285,7 +290,7 @@ export function fileIO(): Omit<IntegrationIO, "appendJournal" | "putRecord" | "d
285
290
  lstatKind,
286
291
  readText: path => {
287
292
  try {
288
- return { kind: "text", text: readFileSync(path, "utf8") };
293
+ return { kind: "text", text: readBoundedRegularFileSync(path, MAX_INTEGRATION_CONFIG_FILE_BYTES).toString("utf8") };
289
294
  } catch (error) {
290
295
  const code = (error as NodeJS.ErrnoException).code;
291
296
  return code === "ENOENT" ? { kind: "missing" } : { kind: "failed", ...(code ? { code } : {}) };
@@ -300,6 +305,19 @@ export function fileIO(): Omit<IntegrationIO, "appendJournal" | "putRecord" | "d
300
305
  }
301
306
  },
302
307
  writeText: (path, text) => {
308
+ /*
309
+ * One bound on both halves of the seam: a file past this size is one
310
+ * readText refuses, so writing it would strand the integration against
311
+ * its own configuration — or, for Cline's pending marker, leave a
312
+ * transaction recovery could never read back.
313
+ */
314
+ const bytes = Buffer.byteLength(text, "utf8");
315
+ if (bytes > MAX_INTEGRATION_CONFIG_FILE_BYTES) {
316
+ throw new IntegrationWriteSizeError(
317
+ `refusing oversized integration write to ${path}: `
318
+ + `${bytes} bytes is past the ${MAX_INTEGRATION_CONFIG_FILE_BYTES}-byte read bound`,
319
+ );
320
+ }
303
321
  const kind = lstatKind(path);
304
322
  if (kind !== "file" && kind !== "missing") {
305
323
  throw new Error(`refusing unsafe integration write target: ${path}`);
@@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto";
16
16
  import { appendFileSync, existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
17
17
  import { join, resolve, sep } from "node:path";
18
18
  import { atomicWriteFile } from "../config";
19
- import { ensureDir, fingerprint, integrationsDir, isOwnershipRecord, type OwnershipRecord } from "./ownership";
19
+ import { ensureDir, fingerprint, integrationsDir, isOwnershipRecord, readRecordsStrict, type OwnershipRecord } from "./ownership";
20
20
  import { isIntegrationClientId, type IntegrationClientId } from "./registry";
21
21
 
22
22
  /**
@@ -171,6 +171,20 @@ export function captureSnapshot(
171
171
  return { kind: "stored", relPath: join("snapshots", clientId, opId) };
172
172
  }
173
173
 
174
+ /** Remove only an unused pre-write snapshot; uncertain or committed evidence survives. */
175
+ export function discardUncommittedSnapshot(
176
+ clientId: IntegrationClientId,
177
+ opId: string,
178
+ dir: string = integrationsDir(),
179
+ ): void {
180
+ const target = containedPath(dir, "snapshots", assertSafeComponent(clientId, "clientId"), assertSafeComponent(opId, "opId"));
181
+ if (findCommittedOperation(opId, dir)
182
+ || Object.values(readRecordsStrict(dir)).some(record => record?.opId === opId)) {
183
+ throw new Error("integration snapshot still belongs to a recorded operation");
184
+ }
185
+ rmSync(target, { force: true });
186
+ }
187
+
174
188
  /** Commit the row. Pruning is post-commit and can never fail the append. */
175
189
  export function appendOperation(entry: JournalEntry, dir: string = integrationsDir()): void {
176
190
  ensureDir(journalPath(dir));
@@ -131,6 +131,7 @@ export const DYNAMIC_SEGMENT = "*";
131
131
  * paths silently have no declaration.
132
132
  */
133
133
  const CLIENT_MANAGED_PATHS = {
134
+ qoder: [["providers", OPENCODE_PROVIDER_ID], ["modelConfigs", "customModels", DYNAMIC_SEGMENT]],
134
135
  opencode: [["provider", OPENCODE_PROVIDER_ID], ["providers", OPENCODE_PROVIDER_ID]],
135
136
  pi: [["providers", OPENCODE_PROVIDER_ID]],
136
137
  omp: [["providers", OPENCODE_PROVIDER_ID]],
@@ -17,6 +17,8 @@ import { dirname, join } from "node:path";
17
17
  import {
18
18
  ClientPathError,
19
19
  buildDroidContribution,
20
+ qoderConfigPath,
21
+ qoderHomeDir,
20
22
  clineConfigPath,
21
23
  clineSettingsDir,
22
24
  droidConfigPath,
@@ -280,6 +282,11 @@ function xdgConfigHome(env: NodeJS.ProcessEnv, home: string): string {
280
282
  }
281
283
 
282
284
  export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientSpec> = {
285
+ qoder: {
286
+ id: "qoder",
287
+ configPath: qoderConfigPath,
288
+ detectDir: (env = process.env, home = homedir()) => qoderHomeDir(env, home),
289
+ },
283
290
  opencode: {
284
291
  id: "opencode",
285
292
  // These take `home` explicitly. The export registry's `destination` reads
@@ -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),
@@ -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,
@@ -162,6 +162,15 @@ function commit(args: CommitArgs): WriteOutcome {
162
162
  }
163
163
  } catch (error) {
164
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
+ }
165
174
  return refuse(clientId, "write_failed", args.state, messageOf(error), args.snapshotPath);
166
175
  }
167
176
  try {
@@ -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
+ }
@@ -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
+ }
package/src/lib/errors.ts CHANGED
@@ -18,6 +18,13 @@ export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
18
18
  */
19
19
  export const SEND_BUDGET_EXHAUSTED_CODE = "request_send_budget_exhausted";
20
20
  export const SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE = "spend_ledger_storage_unavailable";
21
+ /**
22
+ * Wire code for the undeclared-tool guard's deterministic refusal. Endpoint translators
23
+ * read it to keep the refusal non-retryable instead of folding it into the transient
24
+ * upstream-error class. Declared here rather than in the guard module because the shared
25
+ * envelope formatter decides whether the identity survives serialization.
26
+ */
27
+ export const UNDECLARED_TOOL_CALL_ERROR_CODE = "undeclared_tool_call";
21
28
 
22
29
  /** First nonblank string across the canonical upstream error paths, in priority order. */
23
30
  export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {