@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
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The SOCKS5 handshake both raw outbound transports share: method negotiation
3
+ * (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
4
+ * CONNECT exchange.
5
+ *
6
+ * `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
7
+ * `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
8
+ * dial) each own their socket, their timeouts, and what happens after CONNECT;
9
+ * this module only frames bytes through the reader each hands it, so a
10
+ * credentialed `socks5://` proxy URL authenticates identically whether the caller
11
+ * is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
12
+ * greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
13
+ * proxy working for a caller that configured more than it needed.
14
+ */
15
+
16
+ export class Socks5HandshakeError extends Error {
17
+ override readonly name = "Socks5HandshakeError";
18
+ }
19
+
20
+ const SOCKS5_VERSION = 0x05;
21
+ const SOCKS5_NO_AUTH = 0x00;
22
+ const SOCKS5_USER_PASS = 0x02;
23
+ const SOCKS5_CONNECT = 0x01;
24
+ const SOCKS5_DOMAIN = 0x03;
25
+ const SOCKS5_SUCCESS = 0x00;
26
+
27
+ /** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
28
+ export interface Socks5Credentials {
29
+ username?: Uint8Array;
30
+ password?: Uint8Array;
31
+ }
32
+
33
+ export function socks5Credentials(proxy: URL): Socks5Credentials {
34
+ if (!proxy.username && !proxy.password) return {};
35
+ let username: string;
36
+ let password: string;
37
+ try {
38
+ username = decodeURIComponent(proxy.username);
39
+ password = decodeURIComponent(proxy.password);
40
+ } catch {
41
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
42
+ }
43
+ const usernameBytes = new TextEncoder().encode(username);
44
+ const passwordBytes = new TextEncoder().encode(password);
45
+ if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
46
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
47
+ }
48
+ return { username: usernameBytes, password: passwordBytes };
49
+ }
50
+
51
+ /**
52
+ * The slice of a transport's socket reader the handshake needs: exact-length reads
53
+ * that reject on close, on error, or on the transport's own timeout, plus writes.
54
+ * Bytes read past a step stay queued in the transport's reader; the handshake never
55
+ * touches the socket itself.
56
+ */
57
+ export interface Socks5HandshakeReader {
58
+ write(bytes: Uint8Array): void;
59
+ readExact(bytes: number, signal?: AbortSignal): Promise<Uint8Array>;
60
+ }
61
+
62
+ export interface Socks5Target {
63
+ host: string;
64
+ port: number;
65
+ }
66
+
67
+ /** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
68
+ export async function socks5Handshake(
69
+ reader: Socks5HandshakeReader,
70
+ target: Socks5Target,
71
+ credentials: Socks5Credentials,
72
+ signal?: AbortSignal,
73
+ ): Promise<void> {
74
+ const hostBytes = Buffer.from(target.host, "utf8");
75
+ if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
76
+ const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
77
+ reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
78
+ const greeting = await reader.readExact(2, signal);
79
+ if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
80
+ if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
81
+ reader.write(Buffer.from([
82
+ 0x01,
83
+ credentials.username.byteLength,
84
+ ...credentials.username,
85
+ credentials.password.byteLength,
86
+ ...credentials.password,
87
+ ]));
88
+ const auth = await reader.readExact(2, signal);
89
+ if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
90
+ } else if (greeting[1] !== SOCKS5_NO_AUTH) {
91
+ throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
92
+ }
93
+
94
+ reader.write(Buffer.from([
95
+ SOCKS5_VERSION,
96
+ SOCKS5_CONNECT,
97
+ 0x00,
98
+ SOCKS5_DOMAIN,
99
+ hostBytes.byteLength,
100
+ ...hostBytes,
101
+ target.port >> 8,
102
+ target.port & 0xff,
103
+ ]));
104
+ const reply = await reader.readExact(4, signal);
105
+ if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
106
+ if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
107
+ if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
108
+ throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
109
+ }
110
+ const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
111
+ await reader.readExact(addressLength + 2, signal);
112
+ }
@@ -1,3 +1,5 @@
1
+ import { isNonReplayableResponse, markResponseNonReplayable } from "./upstream-retry";
2
+
1
3
  export const TRANSLATOR_MAX_CALL_ARGUMENT_BYTES = 2 * 1024 * 1024;
2
4
  export const TRANSLATOR_MAX_TURN_BYTES = 32 * 1024 * 1024;
3
5
  export const TRANSLATOR_MAX_SSE_EVENT_BYTES = 32 * 1024 * 1024;
@@ -410,5 +412,9 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
410
412
  });
411
413
  if (signal?.aborted) finalize();
412
414
  else signal?.addEventListener("abort", finalize, { once: true });
413
- return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
415
+ const finalizedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
416
+ // The rebuild above drops identity markers. A refusal that must not be replayed
417
+ // stays non-replayable through accounting finalization.
418
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
419
+ return finalizedResponse;
414
420
  }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The Codex settings a Remote Link Home decides for its Children (#6957).
3
+ *
4
+ * A link Child relays every model request to its Home, so everything the Home's proxy applies
5
+ * per request already follows the Home: effort caps, the sub-agent roster and fallback, the
6
+ * injection prompt, sidecar and compaction routing, and the catalog itself (served by
7
+ * `/v1/catalog`). What does NOT follow is the part of the Home's OpenCodex config that the
8
+ * injector writes into the Child's own Codex `config.toml`. This module carries exactly that
9
+ * part, which is every OpenCodex config field the Codex injection plan reads that is a policy
10
+ * decision rather than a fact about the Child's own listener:
11
+ *
12
+ * - `fastMode` -> `[features] fast_mode`
13
+ * - `multiAgentMode` -> the native `features.multi_agent_v2` reconcile (v1 mode)
14
+ * - `webSearchSidecar.enabled` -> the root `web_search` switch
15
+ * - `syncCodexSubagentDefaults` + `injectionModel` + `injectionEffort` -> the managed
16
+ * `[agents]` defaults
17
+ *
18
+ * Root `model_reasoning_effort` and the `desktop.*` picker keys are Codex app settings that
19
+ * OpenCodex never writes on any machine, so they are deliberately not part of this document.
20
+ * Listener facts (port, hostname, Desktop authless and client compaction routing forms, resume
21
+ * history) stay the Child's own because they describe how the Child's Codex reaches the Child.
22
+ *
23
+ * It rides on its own authenticated link read rather than inside the catalog because the
24
+ * catalog bytes are written verbatim to `model_catalog_json`, which Codex parses itself.
25
+ */
26
+ import { hostname } from "node:os";
27
+ import type { OcxConfig } from "../types";
28
+
29
+ export const HOME_CODEX_SETTINGS_VERSION = 1;
30
+ const MULTI_AGENT_MODES = ["v1", "default", "v2"] as const;
31
+ type MultiAgentMode = (typeof MULTI_AGENT_MODES)[number];
32
+
33
+ export interface HomeCodexSettings {
34
+ /** null: the Home leaves `fast_mode` alone. */
35
+ fastMode: boolean | null;
36
+ /** null: the Home has no explicit mode. */
37
+ multiAgentMode: MultiAgentMode | null;
38
+ webSearchSidecarEnabled: boolean;
39
+ subagentDefaults: { model: string; reasoningEffort: string | null } | null;
40
+ }
41
+
42
+ export interface HomeCodexSettingsDocument {
43
+ version: typeof HOME_CODEX_SETTINGS_VERSION;
44
+ home: { name: string; version: string };
45
+ settings: HomeCodexSettings;
46
+ }
47
+
48
+ const NAME_MAX = 255;
49
+ const VALUE_MAX = 256;
50
+
51
+ function safeLabel(value: string, max: number): string | null {
52
+ const trimmed = value.trim();
53
+ if (!trimmed || trimmed.length > max) return null;
54
+ for (const character of trimmed) {
55
+ const code = character.codePointAt(0) ?? 0;
56
+ if (code < 0x20 || code === 0x7f || (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069)) return null;
57
+ }
58
+ return trimmed;
59
+ }
60
+
61
+ export function projectHomeCodexSettings(config: OcxConfig, version: string): HomeCodexSettingsDocument {
62
+ const mode = config.multiAgentMode;
63
+ // Same predicate as `subagentDefaultSyncEffective` (src/config/diagnostics.ts), inlined so
64
+ // this module stays inside the src/link import boundary.
65
+ const sync = config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
66
+ const effort = config.injectionEffort?.trim();
67
+ return {
68
+ version: HOME_CODEX_SETTINGS_VERSION,
69
+ home: { name: safeLabel(hostname(), NAME_MAX) ?? "Home", version },
70
+ settings: {
71
+ fastMode: typeof config.fastMode === "boolean" ? config.fastMode : null,
72
+ multiAgentMode: mode && (MULTI_AGENT_MODES as readonly string[]).includes(mode) ? mode : null,
73
+ webSearchSidecarEnabled: config.webSearchSidecar?.enabled !== false,
74
+ subagentDefaults: sync
75
+ ? { model: config.injectionModel!.trim(), reasoningEffort: effort ? effort : null }
76
+ : null,
77
+ },
78
+ };
79
+ }
80
+
81
+ function exactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
82
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
83
+ const actual = Object.keys(value);
84
+ return actual.length === keys.length && actual.every(key => keys.includes(key));
85
+ }
86
+
87
+ /** Strict parse: an unknown field, a wrong type or an unsafe label rejects the whole document. */
88
+ export function parseHomeCodexSettingsDocument(value: unknown): HomeCodexSettingsDocument | null {
89
+ if (!exactKeys(value, ["version", "home", "settings"]) || value.version !== HOME_CODEX_SETTINGS_VERSION) return null;
90
+ const home = value.home;
91
+ if (!exactKeys(home, ["name", "version"]) || typeof home.name !== "string" || typeof home.version !== "string") return null;
92
+ const name = safeLabel(home.name, NAME_MAX);
93
+ const homeVersion = safeLabel(home.version, 64);
94
+ if (!name || !homeVersion) return null;
95
+ const s = value.settings;
96
+ if (!exactKeys(s, ["fastMode", "multiAgentMode", "webSearchSidecarEnabled", "subagentDefaults"])) return null;
97
+ if (s.fastMode !== null && typeof s.fastMode !== "boolean") return null;
98
+ if (s.multiAgentMode !== null && !(MULTI_AGENT_MODES as readonly unknown[]).includes(s.multiAgentMode)) return null;
99
+ if (typeof s.webSearchSidecarEnabled !== "boolean") return null;
100
+ let subagentDefaults: HomeCodexSettings["subagentDefaults"] = null;
101
+ if (s.subagentDefaults !== null) {
102
+ const d = s.subagentDefaults;
103
+ if (!exactKeys(d, ["model", "reasoningEffort"]) || typeof d.model !== "string") return null;
104
+ const model = safeLabel(d.model, VALUE_MAX);
105
+ if (!model) return null;
106
+ let reasoningEffort: string | null = null;
107
+ if (d.reasoningEffort !== null) {
108
+ if (typeof d.reasoningEffort !== "string" || !/^[a-z][a-z0-9_-]{0,31}$/.test(d.reasoningEffort)) return null;
109
+ reasoningEffort = d.reasoningEffort;
110
+ }
111
+ subagentDefaults = { model, reasoningEffort };
112
+ }
113
+ return {
114
+ version: HOME_CODEX_SETTINGS_VERSION,
115
+ home: { name, version: homeVersion },
116
+ settings: {
117
+ fastMode: s.fastMode as boolean | null,
118
+ multiAgentMode: s.multiAgentMode as MultiAgentMode | null,
119
+ webSearchSidecarEnabled: s.webSearchSidecarEnabled,
120
+ subagentDefaults,
121
+ },
122
+ };
123
+ }
124
+
125
+ /** The Child's config as the Codex injector should see it: local listener facts, Home policy. */
126
+ export function overlayHomeCodexSettings(config: OcxConfig, settings: HomeCodexSettings): OcxConfig {
127
+ const next: OcxConfig = {
128
+ ...config,
129
+ webSearchSidecar: { ...(config.webSearchSidecar ?? {}), enabled: settings.webSearchSidecarEnabled },
130
+ };
131
+ if (settings.fastMode === null) delete next.fastMode;
132
+ else next.fastMode = settings.fastMode;
133
+ if (settings.multiAgentMode === null) delete next.multiAgentMode;
134
+ else next.multiAgentMode = settings.multiAgentMode;
135
+ if (settings.subagentDefaults) {
136
+ next.injectionModel = settings.subagentDefaults.model;
137
+ next.syncCodexSubagentDefaults = true;
138
+ if (settings.subagentDefaults.reasoningEffort) next.injectionEffort = settings.subagentDefaults.reasoningEffort;
139
+ else delete next.injectionEffort;
140
+ } else {
141
+ next.syncCodexSubagentDefaults = false;
142
+ }
143
+ return next;
144
+ }
145
+
146
+ /** One line per applied value, for `ocx sync`, `ocx status` and the dashboard. */
147
+ export function describeHomeCodexSettings(settings: HomeCodexSettings): string[] {
148
+ const d = settings.subagentDefaults;
149
+ return [
150
+ `fast mode: ${settings.fastMode === null ? "unset" : settings.fastMode ? "on" : "off"}`,
151
+ `multi-agent mode: ${settings.multiAgentMode ?? "default"}`,
152
+ `web search sidecar: ${settings.webSearchSidecarEnabled ? "on" : "off"}`,
153
+ `native sub-agent defaults: ${d ? `${d.model}${d.reasoningEffort ? ` (${d.reasoningEffort})` : ""}` : "not synced"}`,
154
+ ];
155
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The read-only management surface a Remote Link Child may reach on its Home (#6957).
3
+ *
4
+ * Default deny: a path is readable only when it is listed here, the method is GET or HEAD, and
5
+ * the query does not ask the Home to refresh upstream state. Everything else is refused on both
6
+ * ends: the Child's listener and CLI refuse before sending, and the Home's link listener answers
7
+ * 404 for any other path under the prefix. No route here returns a credential value; provider
8
+ * and account projections report presence and masked identities only.
9
+ *
10
+ * A link read must not change the Home. The Home runs these reads passively
11
+ * (`src/lib/passive-read.ts`): model discovery, which reaches provider upstreams and persists the
12
+ * initial model selection of a pending provider, config writes and diagnostics do not run (#6959).
13
+ * `tests/server/link-management-relay.test.ts` reads every path here and asserts that the Home
14
+ * config and its upstreams are untouched.
15
+ */
16
+ export const LINK_MANAGEMENT_PREFIX = "/v1/link-management";
17
+ /** The Home's OpenCodex-owned Codex settings a Child applies when it syncs. */
18
+ export const LINK_CODEX_SETTINGS_PATH = `${LINK_MANAGEMENT_PREFIX}/codex-settings`;
19
+
20
+ export const LINK_MANAGEMENT_READ_PATHS: readonly string[] = [
21
+ "/api/system/health",
22
+ "/api/startup-health",
23
+ "/api/providers",
24
+ "/api/settings",
25
+ "/api/usage",
26
+ "/api/effort-caps",
27
+ "/api/injection-model",
28
+ "/api/shadow-call-settings",
29
+ "/api/sidecar-settings",
30
+ "/api/v2",
31
+ "/api/provider-quotas",
32
+ "/api/codex-auth/active",
33
+ "/api/codex-auth/accounts",
34
+ "/api/oauth/accounts",
35
+ "/api/providers/keys",
36
+ "/api/oauth/providers",
37
+ "/api/catalog",
38
+ "/api/system/memory",
39
+ "/api/provider-presets",
40
+ "/api/provider-context-caps",
41
+ ];
42
+ const READ_PATHS = new Set(LINK_MANAGEMENT_READ_PATHS);
43
+ /**
44
+ * Query keys that make a read trigger upstream work on the Home: forced refreshes and the
45
+ * per-account quota probes `?quota=1` starts on the OAuth-account and key-pool reads.
46
+ * `/api/models`, `/api/model-presets`, `/api/subagent-models` and `/api/subagent-model-fallback`
47
+ * are absent for the same reason: they run live provider discovery and may persist initial model
48
+ * selection. The preset preview exists only to decide a write a Child cannot make.
49
+ */
50
+ const REFRESH_KEYS = ["refresh", "reload", "force", "quota"];
51
+
52
+ /** Is `method path?query` (a Home management path, without the link prefix) readable over the link? */
53
+ export function linkManagementReadAllowed(method: string, pathname: string, search: URLSearchParams | string = ""): boolean {
54
+ const upper = method.toUpperCase();
55
+ if (upper !== "GET" && upper !== "HEAD") return false;
56
+ if (!READ_PATHS.has(pathname)) return false;
57
+ const query = typeof search === "string" ? new URLSearchParams(search) : search;
58
+ return !REFRESH_KEYS.some(key => query.has(key));
59
+ }
60
+
61
+ /** The Home management URL a prefixed link request names, or null when it names none. */
62
+ export function linkManagementTarget(url: URL): URL | null {
63
+ if (!url.pathname.startsWith(`${LINK_MANAGEMENT_PREFIX}/api/`)) return null;
64
+ const target = new URL(url.href);
65
+ target.pathname = url.pathname.slice(LINK_MANAGEMENT_PREFIX.length);
66
+ return target;
67
+ }
68
+
69
+ /** Home side: may the hub-link listener dispatch this request to the link management handler? */
70
+ export function linkManagementRouteAllowed(url: URL, req: Request): boolean {
71
+ if (req.headers.has("upgrade")) return false;
72
+ if (url.pathname === LINK_CODEX_SETTINGS_PATH) return req.method === "GET" && url.search === "";
73
+ const target = linkManagementTarget(url);
74
+ return target !== null && linkManagementReadAllowed(req.method, target.pathname, target.searchParams);
75
+ }
76
+
77
+ export function isLinkManagementPath(pathname: string): boolean {
78
+ return pathname === LINK_MANAGEMENT_PREFIX || pathname.startsWith(`${LINK_MANAGEMENT_PREFIX}/`);
79
+ }
80
+
81
+ /**
82
+ * The one refusal every management surface on a link Child uses: the dashboard relay, the CLI,
83
+ * and the Child's listener. It names the Home and the link and never suggests a relay transport.
84
+ * `homePhrase` is "Home <name>" or "its Home" when the Child does not know the Home's name.
85
+ */
86
+ export function linkChildManagementRefusal(homePhrase: string, linkId: string, operation = "This management command"): string {
87
+ return `This machine is a Remote Link Child managed by ${homePhrase} (link ${linkId}). `
88
+ + `${operation} runs on the Home, not on this Child: use the Home's dashboard or run the command there. No request was sent.`;
89
+ }
@@ -0,0 +1,38 @@
1
+ import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
2
+
3
+ export const LINK_COMMAND_WORK_MS = 15_000;
4
+ export const LINK_COMMAND_MAX_MS = 60_000;
5
+ export const LINK_RELAY_VERIFY_MS = 15_000;
6
+ export const LINK_CATALOG_MAX_BYTES = 4 * 1024 * 1024;
7
+
8
+ /** Probe latency pays for SSH setup without making the remote work window unbounded. */
9
+ export function linkCommandBudget(latencyMs = 0): number {
10
+ const latency = Number.isFinite(latencyMs) ? Math.max(0, latencyMs) : 0;
11
+ return Math.min(LINK_COMMAND_MAX_MS, LINK_COMMAND_WORK_MS + Math.ceil(latency * 4));
12
+ }
13
+
14
+ export function linkRestartBudget(latencyMs = 0): number {
15
+ return MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIMEOUT_MS + linkCommandBudget(latencyMs);
16
+ }
17
+
18
+ /** Credential-free Child HTTP probe. Ignore curlrc/proxies; never follow a redirect. */
19
+ export function remoteChildHttpArgv(port: number, path: "healthz" | "v1/catalog", timeoutMs = LINK_RELAY_VERIFY_MS): string[] {
20
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("invalid Child port");
21
+ const seconds = Math.max(1, Math.ceil(Math.min(LINK_RELAY_VERIFY_MS, timeoutMs) / 1000));
22
+ return ["sh", "-c", 'curl -q --noproxy "*" --silent --show-error --fail --connect-timeout 3 --max-time "$3" --write-out "\\n%{http_code}" "http://127.0.0.1:$1/$2"', "ocx-child-http", String(port), path, String(seconds)];
23
+ }
24
+
25
+ export const REMOTE_CURL_PREFLIGHT = ["sh", "-c", 'command -v curl >/dev/null 2>&1 && curl -q --version >/dev/null 2>&1', "ocx-curl-preflight"];
26
+
27
+ export function parseChildHttp(stdout: string): unknown {
28
+ const match = /\n200$/.exec(stdout.trimEnd());
29
+ if (!match) throw new Error("Child HTTP probe did not return 200");
30
+ return JSON.parse(stdout.trimEnd().slice(0, match.index));
31
+ }
32
+
33
+ export function isChildHealth(value: unknown, role: "standalone" | "client", expectedPort: number): boolean {
34
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
35
+ const body = value as Record<string, unknown>;
36
+ return body.port === expectedPort && body.service === "opencodex" && Number.isInteger(body.pid) && (body.pid as number) > 0
37
+ && (role === "client" ? body.role === "client" : (body.role === undefined || body.role === "standalone") && body.restartCapability === "v1");
38
+ }
@@ -131,15 +131,36 @@ export function buildExecArgv(options: ExecArgvOptions): string[] {
131
131
  ];
132
132
  }
133
133
 
134
+ /** The POSIX shell's exit status for a command it could not find. */
135
+ export const REMOTE_COMMAND_NOT_FOUND = 127;
136
+ /** Reserved prelude status: the remote OS cannot run the POSIX Remote Link workflow. */
137
+ export const REMOTE_PLATFORM_UNSUPPORTED = 125;
138
+
134
139
  /**
135
140
  * The remote sh script for every ocx call. `quoteRemote` single-quotes it, so the login shell
136
141
  * passes it through untouched and `$HOME`/`$PATH` expand in the remote `sh`; the arguments reach
137
- * ocx as `"$@"` without another round of parsing. The fallback directories are appended, so an
138
- * ocx the remote PATH already resolves keeps winning. A missing ocx exits 127.
142
+ * ocx as `"$@"` without another round of parsing. Desktop binaries precede PATH candidates;
143
+ * each candidate must run --version successfully. A missing runnable ocx exits 127.
139
144
  */
140
- export const REMOTE_OCX_SCRIPT = 'PATH="$PATH:$HOME/.bun/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin"; exec ocx "$@"';
141
- /** The POSIX shell's exit status for a command it could not find. */
142
- export const REMOTE_COMMAND_NOT_FOUND = 127;
145
+ export const REMOTE_OCX_SCRIPT = `export PATH="$PATH:$HOME/.bun/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin"
146
+ case "$(uname -s 2>/dev/null)" in Windows*|MSYS*|MINGW*|CYGWIN*) exit ${REMOTE_PLATFORM_UNSUPPORTED};; esac
147
+ try_ocx() {
148
+ if [ -x "$1" ] && "$1" --version >/dev/null 2>&1; then
149
+ executable=$1
150
+ shift
151
+ exec "$executable" "$@"
152
+ fi
153
+ }
154
+ for candidate in "$HOME/.opencodex-desktop/bin/ocx" "/Applications/OpenCodex.app/Contents/MacOS/ocx" "/usr/bin/ocx"; do
155
+ try_ocx "$candidate" "$@"
156
+ done
157
+ remaining=$PATH
158
+ while :; do
159
+ directory=\${remaining%%:*}
160
+ try_ocx "\${directory:-.}/ocx" "$@"
161
+ case "$remaining" in *:*) remaining=\${remaining#*:};; *) break;; esac
162
+ done
163
+ exit ${REMOTE_COMMAND_NOT_FOUND}`;
143
164
 
144
165
  export function remoteOcxArgv(args: readonly string[]): string[] {
145
166
  return ["sh", "-c", REMOTE_OCX_SCRIPT, "ocx", ...args];
@@ -1,6 +1,8 @@
1
1
  import { homedir } from "node:os";
2
2
 
3
3
  const DEFAULT_OUTPUT_BYTES = 64 * 1024;
4
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
5
+ const TUNNEL_OUTPUT_EXIT_GRACE_MS = 250;
4
6
  const HINT_MAX_CHARS = 160;
5
7
  // Written as escapes on purpose: invisible and bidi controls must never sit literally in source.
6
8
  const ANSI_SEQUENCE = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b\[[0-?]*[ -/]*[@-~]|\u001b[@-_]/g;
@@ -93,10 +95,21 @@ export class SshRunnerError extends Error {
93
95
  }
94
96
  }
95
97
 
96
- async function readOutput(stream: ReadableStream<Uint8Array>, maxBytes: number, kill?: () => void, fatalUtf8 = true): Promise<string> {
98
+ async function readOutput(stream: ReadableStream<Uint8Array>, maxBytes: number, kill?: () => void, fatalUtf8 = true, exited?: Promise<number>, signal?: AbortSignal): Promise<string> {
97
99
  const reader = stream.getReader();
98
100
  const chunks: Uint8Array[] = [];
99
101
  let total = 0;
102
+ let finished = false;
103
+ let timer: ReturnType<typeof setTimeout> | undefined;
104
+ const cancel = (): void => { void reader.cancel().catch(() => {}); };
105
+ const onExit = (): void => {
106
+ if (!finished) timer = setTimeout(cancel, TUNNEL_OUTPUT_EXIT_GRACE_MS);
107
+ };
108
+ signal?.addEventListener("abort", cancel, { once: true });
109
+ if (signal?.aborted) cancel();
110
+ // A descendant may inherit a pipe after ssh exits. Cancellation completes pending reads
111
+ // immediately; do not await the underlying source's possibly unbounded cancel promise.
112
+ void exited?.then(onExit, onExit);
100
113
  try {
101
114
  for (;;) {
102
115
  const next = await reader.read();
@@ -104,11 +117,15 @@ async function readOutput(stream: ReadableStream<Uint8Array>, maxBytes: number,
104
117
  total += next.value.byteLength;
105
118
  if (total > maxBytes) {
106
119
  kill?.();
120
+ cancel();
107
121
  throw new SshRunnerError("output_limit", `ssh output exceeded ${maxBytes} bytes`);
108
122
  }
109
123
  chunks.push(next.value);
110
124
  }
111
125
  } finally {
126
+ finished = true;
127
+ if (timer !== undefined) clearTimeout(timer);
128
+ signal?.removeEventListener("abort", cancel);
112
129
  reader.releaseLock();
113
130
  }
114
131
  const bytes = new Uint8Array(total);
@@ -149,8 +166,8 @@ export function createSshRunner(deps: { spawn?: typeof Bun.spawn; timeoutMs?: nu
149
166
  } catch (error) {
150
167
  throw new SshRunnerError("spawn", `could not spawn ${argv[0] ?? "ssh"}`, { cause: error });
151
168
  }
152
- const stdout = readOutput(outputStream(child.stdout), DEFAULT_OUTPUT_BYTES, () => defaultKill(child, "SIGTERM"));
153
- const stderr = readOutput(outputStream(child.stderr), DEFAULT_OUTPUT_BYTES, () => defaultKill(child, "SIGTERM"), false);
169
+ const stdout = readOutput(outputStream(child.stdout), DEFAULT_OUTPUT_BYTES, () => defaultKill(child, "SIGTERM"), true, child.exited);
170
+ const stderr = readOutput(outputStream(child.stderr), DEFAULT_OUTPUT_BYTES, () => defaultKill(child, "SIGTERM"), false, child.exited);
154
171
  void stdout.catch(() => {});
155
172
  void stderr.catch(() => {});
156
173
  return {
@@ -165,9 +182,13 @@ export function createSshRunner(deps: { spawn?: typeof Bun.spawn; timeoutMs?: nu
165
182
 
166
183
  return {
167
184
  async run(argv, options = {}) {
185
+ const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
186
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
187
+ throw new SshRunnerError("timeout", "timeoutMs must be a positive integer at most 2147483647");
188
+ }
168
189
  const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_OUTPUT_BYTES;
169
- if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 0) {
170
- throw new SshRunnerError("output_limit", "maxOutputBytes must be a non-negative integer");
190
+ if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 0 || maxOutputBytes > MAX_OUTPUT_BYTES) {
191
+ throw new SshRunnerError("output_limit", `maxOutputBytes must be an integer between 0 and ${MAX_OUTPUT_BYTES}`);
171
192
  }
172
193
  let child: Bun.Subprocess;
173
194
  try {
@@ -181,41 +202,50 @@ export function createSshRunner(deps: { spawn?: typeof Bun.spawn; timeoutMs?: nu
181
202
  throw new SshRunnerError("spawn", `could not spawn ${argv[0] ?? "ssh"}`, { cause: error });
182
203
  }
183
204
 
184
- const stdout = readOutput(outputStream(child.stdout), Math.min(maxOutputBytes, DEFAULT_OUTPUT_BYTES), () => defaultKill(child, "SIGTERM"));
185
- const stderr = readOutput(outputStream(child.stderr), Math.min(maxOutputBytes, DEFAULT_OUTPUT_BYTES), () => defaultKill(child, "SIGTERM"), false);
186
- if (options.stdin !== undefined) {
187
- try {
188
- const input = child.stdin;
189
- if (!input || typeof input === "number") throw new Error("ssh stdin was not piped");
190
- await input.write(options.stdin);
191
- await input.end();
192
- } catch (error) {
193
- child.kill("SIGTERM");
194
- throw new SshRunnerError("spawn", "could not write ssh stdin", { cause: error });
195
- }
196
- }
197
-
198
- const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
199
- let timedOut = false;
205
+ const cancellation = new AbortController();
206
+ let stdout: Promise<string> | undefined;
207
+ let stderr: Promise<string> | undefined;
200
208
  let timer: ReturnType<typeof setTimeout> | undefined;
201
209
  const timeout = new Promise<never>((_, reject) => {
202
210
  timer = setTimeout(() => {
203
- timedOut = true;
204
- defaultKill(child, "SIGTERM");
205
211
  reject(new SshRunnerError("timeout", `ssh command exceeded ${timeoutMs}ms`));
206
212
  }, timeoutMs);
207
213
  });
208
214
  try {
209
- const code = await Promise.race([child.exited, timeout]);
210
- const [out, err] = await Promise.all([stdout, stderr]);
211
- if (timedOut) throw new SshRunnerError("timeout", `ssh command exceeded ${timeoutMs}ms`);
215
+ const outStream = outputStream(child.stdout), errStream = outputStream(child.stderr);
216
+ stdout = readOutput(outStream, maxOutputBytes, undefined, true, undefined, cancellation.signal);
217
+ stderr = readOutput(errStream, Math.min(maxOutputBytes, DEFAULT_OUTPUT_BYTES), undefined, false, undefined, cancellation.signal);
218
+ const stdin = async (): Promise<void> => {
219
+ if (options.stdin === undefined) return;
220
+ try {
221
+ const input = child.stdin;
222
+ if (!input || typeof input === "number") throw new Error("ssh stdin was not piped");
223
+ await input.write(options.stdin);
224
+ // A timed-out write may settle later; do not start another operation after failure.
225
+ if (cancellation.signal.aborted) return;
226
+ await input.end();
227
+ } catch (error) {
228
+ throw new SshRunnerError("spawn", "could not write ssh stdin", { cause: error });
229
+ }
230
+ };
231
+ // Observe all rejections immediately, including errors arriving after timeout. One
232
+ // deadline covers stdin, exit and EOF: an inherited output pipe is still unfinished work.
233
+ const [, code, out, err] = await Promise.race([
234
+ Promise.all([stdin(), child.exited, stdout, stderr]), timeout,
235
+ ]);
212
236
  return { code, stdout: out, stderr: err };
213
237
  } catch (error) {
238
+ cancellation.abort();
239
+ // Only terminate this local SSH child. A kill failure must not hide the original error.
240
+ try { defaultKill(child, "SIGTERM"); } catch { /* Preserve the command failure when an already-gone child cannot be signalled. */ }
214
241
  if (error instanceof SshRunnerError) throw error;
215
242
  throw new SshRunnerError("spawn", "ssh command failed", { cause: error });
216
243
  } finally {
217
244
  if (timer !== undefined) clearTimeout(timer);
218
- void Promise.allSettled([stdout, stderr, child.exited]);
245
+ void child.exited.catch(() => {});
246
+ // reader.cancel closes pending reads even if the source's cancel promise never settles.
247
+ // Do not await stdin or process exit: neither is guaranteed to settle after SIGTERM.
248
+ await Promise.allSettled([stdout, stderr]);
219
249
  }
220
250
  },
221
251
  spawnTunnel,
@@ -227,3 +257,4 @@ export function createSshRunner(deps: { spawn?: typeof Bun.spawn; timeoutMs?: nu
227
257
  }
228
258
 
229
259
  export const SSH_OUTPUT_CAP_BYTES = DEFAULT_OUTPUT_BYTES;
260
+ export const SSH_MAX_OUTPUT_BYTES = MAX_OUTPUT_BYTES;