cartethyia 2.5.9 → 2.6.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 (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -1,300 +1,300 @@
1
- // Cursor Run transport: one validated HTTP/2 bidi stream, framed.
2
- //
3
- // This module owns the wire and nothing else. It opens the stream through the
4
- // validated HTTP/2 binding, writes Connect frames, decodes response frames back
5
- // into protobuf messages, and maps stream/HTTP/gRPC outcomes onto typed errors.
6
- // It never calls `node:http2.connect` itself — the binding owns the socket, the
7
- // DNS pinning, the SSRF policy, and the pool tunnel.
8
- import { GatewayError } from "../../../protocol/gateway-error";
9
- import type { ValidatedHttp2BidiStream, Http2BidiEndReason } from "../../../network/http2-bidi";
10
- import type { ValidatedOutboundHttp2Bidi } from "../../provider-contracts";
11
- import { decodeFramePayload, encodeFrame, streamFrames, FLAG_END_STREAM } from "./protocol/framing";
12
- import { decodeMessage } from "./protocol/messages";
13
- import {
14
- CONTENT_TYPE_CONNECT_PROTO,
15
- CONNECT_PROTOCOL_VERSION,
16
- FALLBACK_CLIENT_VERSION,
17
- RUN_PATH,
18
- isAllowedAgentHost,
19
- } from "./cursor-shared";
20
- import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
21
-
22
- import { isRecord } from "../../../protocol/codecs";
23
-
24
- /**
25
- * The refusal Cursor sends inside an otherwise-successful 200 Run stream.
26
- *
27
- * It is a Connect JSON error envelope, not protobuf, and it is where a real
28
- * account-level refusal lands — a rate-limited or exhausted account streams a
29
- * heartbeat and then this. Without it the Run looks like a deadlock: the
30
- * stream opened, no text ever arrived, and nothing named the reason.
31
- */
32
- function connectErrorEnvelope(payload: Uint8Array): GatewayError | undefined {
33
- const text = new TextDecoder().decode(payload);
34
- if (!text.trimStart().startsWith("{\"error\"")) return undefined;
35
- let parsed: unknown;
36
- try {
37
- parsed = JSON.parse(text);
38
- } catch {
39
- return undefined;
40
- }
41
- if (!isRecord(parsed) || !isRecord(parsed["error"])) return undefined;
42
- const error = parsed["error"];
43
- const code = typeof error["code"] === "string" ? error["code"] : "";
44
- const detail = cursorErrorDetail(error);
45
- if (code === "resource_exhausted") {
46
- return new GatewayError(
47
- "quota_exceeded",
48
- 429,
49
- `Cursor refused the Run: ${detail}`,
50
- { providerId: "cursor", upstreamCode: code },
51
- "upstream",
52
- );
53
- }
54
- return new GatewayError(
55
- "platform_unavailable",
56
- 502,
57
- `Cursor refused the Run: ${detail}`,
58
- { providerId: "cursor", upstreamCode: code },
59
- "upstream",
60
- );
61
- }
62
-
63
- /** The human-readable refusal, preferring Cursor's own `chatMessage`/`detail`. */
64
- function cursorErrorDetail(error: Record<string, unknown>): string {
65
- const raw = typeof error["message"] === "string" ? error["message"] : "";
66
- for (const detail of Array.isArray(error["details"]) ? error["details"] : []) {
67
- if (!isRecord(detail)) continue;
68
- const debug = detail["debug"];
69
- if (!isRecord(debug)) continue;
70
- const info = debug["details"];
71
- if (isRecord(info)) {
72
- const chat = info["chatMessage"];
73
- if (typeof chat === "string" && chat.length > 0) return chat;
74
- const title = info["title"];
75
- if (typeof title === "string" && title.length > 0) return title;
76
- }
77
- }
78
- return raw.length > 0 ? raw : "Cursor refused the Run";
79
- }
80
-
81
- /** One decoded Cursor server message. */
82
- export interface CursorServerMessage {
83
- readonly raw: Record<string, unknown>;
84
- }
85
-
86
- /** Read-idle bound for one Run stream. */
87
- const DEFAULT_READ_IDLE_MS = 120_000;
88
-
89
- /** Cursor identity headers for the Run stream. */
90
- async function runHeaders(credential: string, clientVersion: string): Promise<Record<string, string>> {
91
- const { machineId, macMachineId } = await resolveCursorDeviceIds();
92
- return {
93
- authorization: `Bearer ${credential}`,
94
- "content-type": CONTENT_TYPE_CONNECT_PROTO,
95
- "connect-protocol-version": CONNECT_PROTOCOL_VERSION,
96
- "connect-accept-encoding": "gzip,br",
97
- "x-cursor-client-type": "cli",
98
- "x-cursor-client-version": clientVersion,
99
- "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
100
- // The CLI's streaming interceptor sets this on the bidi Run stream.
101
- // Without it Cursor may treat the stream as non-streaming.
102
- "x-cursor-streaming": "true",
103
- "x-ghost-mode": "true",
104
- "x-request-id": crypto.randomUUID(),
105
- "user-agent": "connect-es/1.6.1",
106
- };
107
- }
108
-
109
- /**
110
- * Opens one Cursor Run stream.
111
- *
112
- * The host is re-validated here as well as in the binding: the Run origin comes
113
- * from `GetServerConfig`, which is upstream data, so it is checked against the
114
- * Cursor allowlist before a credentialed connection is opened to it.
115
- */
116
- export async function openCursorRun(input: {
117
- readonly origin: string;
118
- readonly credential: string;
119
- readonly outboundHttp2: ValidatedOutboundHttp2Bidi;
120
- readonly signal: AbortSignal;
121
- readonly clientVersion?: string;
122
- readonly readIdleMs?: number;
123
- }): Promise<{ stream: ValidatedHttp2BidiStream; write: (message: Uint8Array) => Promise<void> }> {
124
- const url = new URL(`${input.origin}${RUN_PATH}`);
125
- if (url.protocol !== "https:" || !isAllowedAgentHost(url.hostname)) {
126
- throw new GatewayError(
127
- "invalid_request",
128
- 400,
129
- "Cursor Run requires an allowlisted Cursor agent origin",
130
- { origin: input.origin },
131
- );
132
- }
133
- const headers = await runHeaders(input.credential, input.clientVersion ?? FALLBACK_CLIENT_VERSION);
134
- const stream = await input.outboundHttp2(url, headers, {
135
- signal: input.signal,
136
- readIdleMs: input.readIdleMs ?? DEFAULT_READ_IDLE_MS,
137
- allowHostname: isAllowedAgentHost,
138
- });
139
- return {
140
- stream,
141
- write: async (message: Uint8Array): Promise<void> => {
142
- await stream.write(encodeFrame(0, message));
143
- },
144
- };
145
- }
146
-
147
- /** Map of gRPC status codes onto gateway errors. */
148
- function grpcError(status: string, message: string | undefined): GatewayError {
149
- const code = Number(status);
150
- const detail = message === undefined || message.length === 0 ? "" : `: ${message}`;
151
- if (code === 16 || code === 7) {
152
- // UNAUTHENTICATED / PERMISSION_DENIED: the credential, not the request.
153
- return new GatewayError(
154
- "authentication_failed",
155
- 401,
156
- `Cursor refused the credential${detail}`,
157
- { grpcStatus: status },
158
- "upstream",
159
- );
160
- }
161
- if (code === 8 || code === 14) {
162
- return new GatewayError(
163
- "quota_exceeded",
164
- 429,
165
- `Cursor refused the Run${detail}`,
166
- { grpcStatus: status },
167
- "upstream",
168
- );
169
- }
170
- if (code === 4 || code === 10) {
171
- return new GatewayError(
172
- "deadline_exceeded",
173
- 504,
174
- `Cursor Run ended before it finished${detail}`,
175
- { grpcStatus: status },
176
- "upstream",
177
- );
178
- }
179
- return new GatewayError(
180
- "platform_unavailable",
181
- 502,
182
- `Cursor Run failed (gRPC ${status})${detail}`,
183
- { grpcStatus: status },
184
- "upstream",
185
- );
186
- }
187
-
188
- /**
189
- * The terminal error for one ended Run stream.
190
- *
191
- * Status, trailers, and transport error are read in that order because that is
192
- * the order of authority: an HTTP status is what the edge answered, a gRPC
193
- * trailer is what the server decided, and a transport error is what the socket
194
- * did. A clean close with none of them is still a failure — a Run that ends
195
- * without `turn_ended` produced no answer.
196
- */
197
- export function cursorRunTerminationError(input: {
198
- readonly stream: ValidatedHttp2BidiStream;
199
- readonly reason: Http2BidiEndReason;
200
- }): GatewayError | undefined {
201
- if (input.reason.kind === "local-close") return undefined;
202
- if (input.reason.kind === "remote-error") {
203
- return input.reason.error instanceof GatewayError
204
- ? input.reason.error
205
- : new GatewayError(
206
- "transport_unavailable",
207
- 502,
208
- `Cursor Run transport interrupted: ${input.reason.error.message}`,
209
- {},
210
- "network",
211
- );
212
- }
213
- const { status, headers } = input.stream.response;
214
- if (status !== 0 && status !== 200) {
215
- const message = headers["grpc-message"];
216
- return new GatewayError(
217
- status === 401 || status === 403 ? "authentication_failed" : "platform_unavailable",
218
- status === 401 || status === 403 ? 401 : 502,
219
- `Cursor Run failed by remote: ${status}`,
220
- { httpStatus: status, ...(message === undefined ? {} : { upstreamMessage: message }) },
221
- "upstream",
222
- );
223
- }
224
- const trailers = input.stream.trailers;
225
- const grpcStatus = trailers["grpc-status"] ?? headers["grpc-status"];
226
- if (grpcStatus !== undefined && grpcStatus !== "0") {
227
- return grpcError(grpcStatus, trailers["grpc-message"] ?? headers["grpc-message"]);
228
- }
229
- return new GatewayError(
230
- "platform_unavailable",
231
- 502,
232
- "Cursor Run ended before turn_ended",
233
- {},
234
- "upstream",
235
- );
236
- }
237
-
238
- /**
239
- * Decodes a response byte stream into Cursor server messages.
240
- *
241
- * Buffering is bounded and the leftover is carried forward rather than
242
- * re-merged from scratch: a Run frame can be many megabytes, and copying the
243
- * whole pending buffer on every chunk is what turns a long Run into a stall.
244
- */
245
- export async function* decodeServerMessages(
246
- body: AsyncIterable<Uint8Array>,
247
- maxBufferedBytes = 64 * 1024 * 1024,
248
- ): AsyncGenerator<Record<string, unknown>, void, void> {
249
- let pending: Uint8Array = new Uint8Array(0);
250
- for await (const chunk of body) {
251
- pending = pending.byteLength === 0 ? chunk : concat(pending, chunk);
252
- if (pending.byteLength > maxBufferedBytes) {
253
- throw new GatewayError(
254
- "platform_unavailable",
255
- 502,
256
- "Cursor Run exceeded the frame buffer bound",
257
- {},
258
- "upstream",
259
- );
260
- }
261
- const frames = [...streamFrames(pending)];
262
- if (frames.length === 0) continue;
263
- let consumed = 0;
264
- for (const frame of frames) consumed += 5 + frame.payload.byteLength;
265
- const leftover = pending.subarray(consumed);
266
- pending = leftover.byteLength === 0 ? new Uint8Array(0) : new Uint8Array(leftover);
267
- for (const frame of frames) {
268
- const payload = decodeFramePayload(frame);
269
- // A refusal is a Connect JSON error envelope, not protobuf: an exhausted
270
- // account answers with one inside a normal 200 stream. Surfacing it is
271
- // the difference between "the model said nothing" and "you hit your
272
- // usage limit".
273
- const envelope = connectErrorEnvelope(payload);
274
- if (envelope !== undefined) throw envelope;
275
- try {
276
- yield decodeMessage("AgentServerMessage", payload);
277
- } catch {
278
- // One undecodable frame is not a protocol failure to abort on: Cursor
279
- // extends this schema with fields this vendored copy does not model, and
280
- // a Run that produced usable output must not die on one unknown field.
281
- continue;
282
- }
283
- // `END_STREAM` ends the *response*, not the payload: Cursor sets it on the
284
- // last frame of a Run, which is exactly the frame carrying `turn_ended`
285
- // and the model's final text. Returning before decoding it dropped every
286
- // answer and left the Run looking like a heartbeat-only deadlock.
287
- if ((frame.flags & FLAG_END_STREAM) !== 0) return;
288
- }
289
- }
290
- }
291
-
292
- function concat(left: Uint8Array, right: Uint8Array): Uint8Array {
293
- const out = new Uint8Array(left.byteLength + right.byteLength);
294
- out.set(left, 0);
295
- out.set(right, left.byteLength);
296
- return out;
297
- }
298
-
299
- /** Heartbeat cadence while a Run is held open waiting for a tool result. */
300
- export const RUN_HEARTBEAT_INTERVAL_MS = 20_000;
1
+ // Cursor Run transport: one validated HTTP/2 bidi stream, framed.
2
+ //
3
+ // This module owns the wire and nothing else. It opens the stream through the
4
+ // validated HTTP/2 binding, writes Connect frames, decodes response frames back
5
+ // into protobuf messages, and maps stream/HTTP/gRPC outcomes onto typed errors.
6
+ // It never calls `node:http2.connect` itself — the binding owns the socket, the
7
+ // DNS pinning, the SSRF policy, and the pool tunnel.
8
+ import { GatewayError } from "../../../protocol/gateway-error";
9
+ import type { ValidatedHttp2BidiStream, Http2BidiEndReason } from "../../../network/http2-bidi";
10
+ import type { ValidatedOutboundHttp2Bidi } from "../../provider-contracts";
11
+ import { decodeFramePayload, encodeFrame, streamFrames, FLAG_END_STREAM } from "./protocol/framing";
12
+ import { decodeMessage } from "./protocol/messages";
13
+ import {
14
+ CONTENT_TYPE_CONNECT_PROTO,
15
+ CONNECT_PROTOCOL_VERSION,
16
+ FALLBACK_CLIENT_VERSION,
17
+ RUN_PATH,
18
+ isAllowedAgentHost,
19
+ } from "./cursor-shared";
20
+ import { createCursorChecksumHeader, resolveCursorDeviceIds } from "./cursor-identity";
21
+
22
+ import { isRecord } from "../../../protocol/codecs";
23
+
24
+ /**
25
+ * The refusal Cursor sends inside an otherwise-successful 200 Run stream.
26
+ *
27
+ * It is a Connect JSON error envelope, not protobuf, and it is where a real
28
+ * account-level refusal lands — a rate-limited or exhausted account streams a
29
+ * heartbeat and then this. Without it the Run looks like a deadlock: the
30
+ * stream opened, no text ever arrived, and nothing named the reason.
31
+ */
32
+ function connectErrorEnvelope(payload: Uint8Array): GatewayError | undefined {
33
+ const text = new TextDecoder().decode(payload);
34
+ if (!text.trimStart().startsWith("{\"error\"")) return undefined;
35
+ let parsed: unknown;
36
+ try {
37
+ parsed = JSON.parse(text);
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ if (!isRecord(parsed) || !isRecord(parsed["error"])) return undefined;
42
+ const error = parsed["error"];
43
+ const code = typeof error["code"] === "string" ? error["code"] : "";
44
+ const detail = cursorErrorDetail(error);
45
+ if (code === "resource_exhausted") {
46
+ return new GatewayError(
47
+ "quota_exceeded",
48
+ 429,
49
+ `Cursor refused the Run: ${detail}`,
50
+ { providerId: "cursor", upstreamCode: code },
51
+ "upstream",
52
+ );
53
+ }
54
+ return new GatewayError(
55
+ "platform_unavailable",
56
+ 502,
57
+ `Cursor refused the Run: ${detail}`,
58
+ { providerId: "cursor", upstreamCode: code },
59
+ "upstream",
60
+ );
61
+ }
62
+
63
+ /** The human-readable refusal, preferring Cursor's own `chatMessage`/`detail`. */
64
+ function cursorErrorDetail(error: Record<string, unknown>): string {
65
+ const raw = typeof error["message"] === "string" ? error["message"] : "";
66
+ for (const detail of Array.isArray(error["details"]) ? error["details"] : []) {
67
+ if (!isRecord(detail)) continue;
68
+ const debug = detail["debug"];
69
+ if (!isRecord(debug)) continue;
70
+ const info = debug["details"];
71
+ if (isRecord(info)) {
72
+ const chat = info["chatMessage"];
73
+ if (typeof chat === "string" && chat.length > 0) return chat;
74
+ const title = info["title"];
75
+ if (typeof title === "string" && title.length > 0) return title;
76
+ }
77
+ }
78
+ return raw.length > 0 ? raw : "Cursor refused the Run";
79
+ }
80
+
81
+ /** One decoded Cursor server message. */
82
+ export interface CursorServerMessage {
83
+ readonly raw: Record<string, unknown>;
84
+ }
85
+
86
+ /** Read-idle bound for one Run stream. */
87
+ const DEFAULT_READ_IDLE_MS = 120_000;
88
+
89
+ /** Cursor identity headers for the Run stream. */
90
+ async function runHeaders(credential: string, clientVersion: string): Promise<Record<string, string>> {
91
+ const { machineId, macMachineId } = await resolveCursorDeviceIds();
92
+ return {
93
+ authorization: `Bearer ${credential}`,
94
+ "content-type": CONTENT_TYPE_CONNECT_PROTO,
95
+ "connect-protocol-version": CONNECT_PROTOCOL_VERSION,
96
+ "connect-accept-encoding": "gzip,br",
97
+ "x-cursor-client-type": "cli",
98
+ "x-cursor-client-version": clientVersion,
99
+ "x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
100
+ // The CLI's streaming interceptor sets this on the bidi Run stream.
101
+ // Without it Cursor may treat the stream as non-streaming.
102
+ "x-cursor-streaming": "true",
103
+ "x-ghost-mode": "true",
104
+ "x-request-id": crypto.randomUUID(),
105
+ "user-agent": "connect-es/1.6.1",
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Opens one Cursor Run stream.
111
+ *
112
+ * The host is re-validated here as well as in the binding: the Run origin comes
113
+ * from `GetServerConfig`, which is upstream data, so it is checked against the
114
+ * Cursor allowlist before a credentialed connection is opened to it.
115
+ */
116
+ export async function openCursorRun(input: {
117
+ readonly origin: string;
118
+ readonly credential: string;
119
+ readonly outboundHttp2: ValidatedOutboundHttp2Bidi;
120
+ readonly signal: AbortSignal;
121
+ readonly clientVersion?: string;
122
+ readonly readIdleMs?: number;
123
+ }): Promise<{ stream: ValidatedHttp2BidiStream; write: (message: Uint8Array) => Promise<void> }> {
124
+ const url = new URL(`${input.origin}${RUN_PATH}`);
125
+ if (url.protocol !== "https:" || !isAllowedAgentHost(url.hostname)) {
126
+ throw new GatewayError(
127
+ "invalid_request",
128
+ 400,
129
+ "Cursor Run requires an allowlisted Cursor agent origin",
130
+ { origin: input.origin },
131
+ );
132
+ }
133
+ const headers = await runHeaders(input.credential, input.clientVersion ?? FALLBACK_CLIENT_VERSION);
134
+ const stream = await input.outboundHttp2(url, headers, {
135
+ signal: input.signal,
136
+ readIdleMs: input.readIdleMs ?? DEFAULT_READ_IDLE_MS,
137
+ allowHostname: isAllowedAgentHost,
138
+ });
139
+ return {
140
+ stream,
141
+ write: async (message: Uint8Array): Promise<void> => {
142
+ await stream.write(encodeFrame(0, message));
143
+ },
144
+ };
145
+ }
146
+
147
+ /** Map of gRPC status codes onto gateway errors. */
148
+ function grpcError(status: string, message: string | undefined): GatewayError {
149
+ const code = Number(status);
150
+ const detail = message === undefined || message.length === 0 ? "" : `: ${message}`;
151
+ if (code === 16 || code === 7) {
152
+ // UNAUTHENTICATED / PERMISSION_DENIED: the credential, not the request.
153
+ return new GatewayError(
154
+ "authentication_failed",
155
+ 401,
156
+ `Cursor refused the credential${detail}`,
157
+ { grpcStatus: status },
158
+ "upstream",
159
+ );
160
+ }
161
+ if (code === 8 || code === 14) {
162
+ return new GatewayError(
163
+ "quota_exceeded",
164
+ 429,
165
+ `Cursor refused the Run${detail}`,
166
+ { grpcStatus: status },
167
+ "upstream",
168
+ );
169
+ }
170
+ if (code === 4 || code === 10) {
171
+ return new GatewayError(
172
+ "deadline_exceeded",
173
+ 504,
174
+ `Cursor Run ended before it finished${detail}`,
175
+ { grpcStatus: status },
176
+ "upstream",
177
+ );
178
+ }
179
+ return new GatewayError(
180
+ "platform_unavailable",
181
+ 502,
182
+ `Cursor Run failed (gRPC ${status})${detail}`,
183
+ { grpcStatus: status },
184
+ "upstream",
185
+ );
186
+ }
187
+
188
+ /**
189
+ * The terminal error for one ended Run stream.
190
+ *
191
+ * Status, trailers, and transport error are read in that order because that is
192
+ * the order of authority: an HTTP status is what the edge answered, a gRPC
193
+ * trailer is what the server decided, and a transport error is what the socket
194
+ * did. A clean close with none of them is still a failure — a Run that ends
195
+ * without `turn_ended` produced no answer.
196
+ */
197
+ export function cursorRunTerminationError(input: {
198
+ readonly stream: ValidatedHttp2BidiStream;
199
+ readonly reason: Http2BidiEndReason;
200
+ }): GatewayError | undefined {
201
+ if (input.reason.kind === "local-close") return undefined;
202
+ if (input.reason.kind === "remote-error") {
203
+ return input.reason.error instanceof GatewayError
204
+ ? input.reason.error
205
+ : new GatewayError(
206
+ "transport_unavailable",
207
+ 502,
208
+ `Cursor Run transport interrupted: ${input.reason.error.message}`,
209
+ {},
210
+ "network",
211
+ );
212
+ }
213
+ const { status, headers } = input.stream.response;
214
+ if (status !== 0 && status !== 200) {
215
+ const message = headers["grpc-message"];
216
+ return new GatewayError(
217
+ status === 401 || status === 403 ? "authentication_failed" : "platform_unavailable",
218
+ status === 401 || status === 403 ? 401 : 502,
219
+ `Cursor Run failed by remote: ${status}`,
220
+ { httpStatus: status, ...(message === undefined ? {} : { upstreamMessage: message }) },
221
+ "upstream",
222
+ );
223
+ }
224
+ const trailers = input.stream.trailers;
225
+ const grpcStatus = trailers["grpc-status"] ?? headers["grpc-status"];
226
+ if (grpcStatus !== undefined && grpcStatus !== "0") {
227
+ return grpcError(grpcStatus, trailers["grpc-message"] ?? headers["grpc-message"]);
228
+ }
229
+ return new GatewayError(
230
+ "platform_unavailable",
231
+ 502,
232
+ "Cursor Run ended before turn_ended",
233
+ {},
234
+ "upstream",
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Decodes a response byte stream into Cursor server messages.
240
+ *
241
+ * Buffering is bounded and the leftover is carried forward rather than
242
+ * re-merged from scratch: a Run frame can be many megabytes, and copying the
243
+ * whole pending buffer on every chunk is what turns a long Run into a stall.
244
+ */
245
+ export async function* decodeServerMessages(
246
+ body: AsyncIterable<Uint8Array>,
247
+ maxBufferedBytes = 64 * 1024 * 1024,
248
+ ): AsyncGenerator<Record<string, unknown>, void, void> {
249
+ let pending: Uint8Array = new Uint8Array(0);
250
+ for await (const chunk of body) {
251
+ pending = pending.byteLength === 0 ? chunk : concat(pending, chunk);
252
+ if (pending.byteLength > maxBufferedBytes) {
253
+ throw new GatewayError(
254
+ "platform_unavailable",
255
+ 502,
256
+ "Cursor Run exceeded the frame buffer bound",
257
+ {},
258
+ "upstream",
259
+ );
260
+ }
261
+ const frames = [...streamFrames(pending)];
262
+ if (frames.length === 0) continue;
263
+ let consumed = 0;
264
+ for (const frame of frames) consumed += 5 + frame.payload.byteLength;
265
+ const leftover = pending.subarray(consumed);
266
+ pending = leftover.byteLength === 0 ? new Uint8Array(0) : new Uint8Array(leftover);
267
+ for (const frame of frames) {
268
+ const payload = decodeFramePayload(frame);
269
+ // A refusal is a Connect JSON error envelope, not protobuf: an exhausted
270
+ // account answers with one inside a normal 200 stream. Surfacing it is
271
+ // the difference between "the model said nothing" and "you hit your
272
+ // usage limit".
273
+ const envelope = connectErrorEnvelope(payload);
274
+ if (envelope !== undefined) throw envelope;
275
+ try {
276
+ yield decodeMessage("AgentServerMessage", payload);
277
+ } catch {
278
+ // One undecodable frame is not a protocol failure to abort on: Cursor
279
+ // extends this schema with fields this vendored copy does not model, and
280
+ // a Run that produced usable output must not die on one unknown field.
281
+ continue;
282
+ }
283
+ // `END_STREAM` ends the *response*, not the payload: Cursor sets it on the
284
+ // last frame of a Run, which is exactly the frame carrying `turn_ended`
285
+ // and the model's final text. Returning before decoding it dropped every
286
+ // answer and left the Run looking like a heartbeat-only deadlock.
287
+ if ((frame.flags & FLAG_END_STREAM) !== 0) return;
288
+ }
289
+ }
290
+ }
291
+
292
+ function concat(left: Uint8Array, right: Uint8Array): Uint8Array {
293
+ const out = new Uint8Array(left.byteLength + right.byteLength);
294
+ out.set(left, 0);
295
+ out.set(right, left.byteLength);
296
+ return out;
297
+ }
298
+
299
+ /** Heartbeat cadence while a Run is held open waiting for a tool result. */
300
+ export const RUN_HEARTBEAT_INTERVAL_MS = 20_000;
@@ -1,3 +1,4 @@
1
+ import { sha256Hex } from "../../../shared/hash";
1
2
  // Stable, non-secret installation identity for the Cursor integration.
2
3
  //
3
4
  // Cursor keys device identity off the ids embedded in `x-cursor-checksum` and
@@ -12,16 +13,13 @@
12
13
  // installation and is never derived from a credential. A credential-derived id
13
14
  // would rotate whenever an operator re-authenticated, which is exactly the
14
15
  // "too many devices" symptom the stability exists to avoid.
15
- import { createHash } from "node:crypto";
16
16
  import { getOrCreateInstallId } from "../shared/install-id";
17
17
 
18
18
  /** One process-lifetime cache: the id is stable, so it is read once. */
19
19
  let cached: { machineId: string; macMachineId: string } | undefined;
20
20
  let inFlight: Promise<{ machineId: string; macMachineId: string }> | undefined;
21
21
 
22
- function sha256Hex(value: string): string {
23
- return createHash("sha256").update(value, "utf8").digest("hex");
24
- }
22
+
25
23
 
26
24
  /**
27
25
  * Stable device fingerprints for the `x-cursor-checksum` header.
@@ -17,6 +17,17 @@
17
17
  import type { CanonicalMessage, CanonicalRequest, ContentPart, ToolDefinition } from "../../../protocol/canonical-model";
18
18
  import { encodeMessage } from "./protocol/messages";
19
19
  import { encodeJsonAsValue } from "./protocol/struct";
20
+ // Inbound half of the tool surface. `toolDescriptorsFor` below is the outbound
21
+ // half — what this request advertises. Together they are the only two places a
22
+ // Cursor tool is named, so both live here rather than one in a protocol module
23
+ // a caller has to discover separately.
24
+ export {
25
+ CURSOR_TOOL_VARIANTS,
26
+ CursorToolCallProvenance,
27
+ cursorNativeToolDeclaration,
28
+ cursorToolArm,
29
+ decodeCursorNativeToolCall,
30
+ } from "./protocol/tool-canonical";
20
31
 
21
32
  /**
22
33
  * MCP namespace Cartethyia advertises client tools under.