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.
- package/.env.example +15 -42
- package/README.md +11 -10
- package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -97
- package/scripts/cli/admin.ts +126 -125
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +308 -275
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/doctor.ts +1 -1
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -261
- package/scripts/{health.ts → cli/health.ts} +4 -4
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -164
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +275 -107
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -82
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +192 -208
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/package-paths.ts +24 -0
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/remote-support/service.ts +270 -114
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/routing/pools/store.ts +1 -1
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/repository-updates.ts +40 -39
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/registry.ts +2 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +14 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/capabilities.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/github/github.ts +3 -1
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/opencode/provider.ts +2 -0
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-headers.ts +17 -48
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/trae/trae.ts +22 -24
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/provider-probe.ts +3 -0
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
- package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
- package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
- package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/scripts/build/aot.ts +0 -143
- package/scripts/build/build.ts +0 -24
- package/scripts/build/compile.ts +0 -88
- package/scripts/build/restart.ts +0 -120
- package/scripts/build/start.ts +0 -25
- package/src/observability/event-compaction.ts +0 -317
- 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
|
-
|
|
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.
|