@vellumai/assistant 0.4.46 → 0.4.49

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 (382) hide show
  1. package/ARCHITECTURE.md +7 -7
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/security.md +5 -5
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +1 -1
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/anthropic-provider.test.ts +156 -0
  12. package/src/__tests__/approval-cascade.test.ts +810 -0
  13. package/src/__tests__/approval-primitive.test.ts +0 -1
  14. package/src/__tests__/approval-routes-http.test.ts +2 -0
  15. package/src/__tests__/assistant-attachments.test.ts +12 -34
  16. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  17. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  18. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  19. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  20. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  21. package/src/__tests__/channel-guardian.test.ts +0 -2
  22. package/src/__tests__/channel-readiness-routes.test.ts +35 -25
  23. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  24. package/src/__tests__/checker.test.ts +9 -29
  25. package/src/__tests__/cli.test.ts +23 -0
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-watcher.test.ts +0 -1
  29. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  30. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  31. package/src/__tests__/context-token-estimator.test.ts +196 -13
  32. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  33. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  34. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  36. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  37. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  38. package/src/__tests__/credential-broker.test.ts +2 -1
  39. package/src/__tests__/credential-metadata-store.test.ts +239 -26
  40. package/src/__tests__/credential-resolve.test.ts +5 -4
  41. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  42. package/src/__tests__/credential-security-invariants.test.ts +111 -7
  43. package/src/__tests__/credential-vault-unit.test.ts +287 -54
  44. package/src/__tests__/credential-vault.test.ts +406 -12
  45. package/src/__tests__/credentials-cli.test.ts +82 -6
  46. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  47. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  48. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/gemini-image-service.test.ts +75 -45
  51. package/src/__tests__/gemini-provider.test.ts +9 -6
  52. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  53. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  54. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  55. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  56. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  57. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  58. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  59. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  60. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  61. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  62. package/src/__tests__/heartbeat-service.test.ts +0 -1
  63. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  64. package/src/__tests__/host-shell-tool.test.ts +27 -15
  65. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  66. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  67. package/src/__tests__/integration-status.test.ts +38 -25
  68. package/src/__tests__/intent-routing.test.ts +0 -1
  69. package/src/__tests__/invite-routes-http.test.ts +10 -9
  70. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  71. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  72. package/src/__tests__/media-generate-image.test.ts +63 -2
  73. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  74. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +373 -14
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +756 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  83. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  84. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  85. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  86. package/src/__tests__/recording-handler.test.ts +3 -4
  87. package/src/__tests__/registry.test.ts +2 -2
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  91. package/src/__tests__/schema-transforms.test.ts +226 -0
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  95. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  96. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  97. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  98. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  99. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  100. package/src/__tests__/sequence-store.test.ts +0 -1
  101. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  102. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  103. package/src/__tests__/skill-include-graph.test.ts +66 -0
  104. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  105. package/src/__tests__/skill-load-tool.test.ts +149 -1
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  107. package/src/__tests__/skills-uninstall.test.ts +3 -3
  108. package/src/__tests__/skills.test.ts +3 -12
  109. package/src/__tests__/slack-channel-config.test.ts +76 -11
  110. package/src/__tests__/slack-share-routes.test.ts +17 -14
  111. package/src/__tests__/system-prompt.test.ts +0 -1
  112. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  113. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  114. package/src/__tests__/terminal-tools.test.ts +4 -3
  115. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  116. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  117. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  118. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  119. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  120. package/src/__tests__/tool-executor.test.ts +0 -1
  121. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  122. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  123. package/src/__tests__/trust-store.test.ts +1 -22
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  126. package/src/__tests__/twilio-config.test.ts +2 -1
  127. package/src/__tests__/twilio-provider.test.ts +4 -2
  128. package/src/__tests__/twilio-routes.test.ts +5 -20
  129. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  130. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  131. package/src/agent/ax-tree-compaction.test.ts +235 -0
  132. package/src/agent/loop.ts +76 -130
  133. package/src/calls/call-domain.ts +8 -10
  134. package/src/calls/relay-server.ts +9 -13
  135. package/src/calls/twilio-config.ts +4 -8
  136. package/src/calls/twilio-provider.ts +2 -1
  137. package/src/calls/twilio-rest.ts +2 -1
  138. package/src/calls/twilio-routes.ts +1 -2
  139. package/src/calls/voice-ingress-preflight.ts +1 -1
  140. package/src/cli/commands/browser-relay.ts +46 -15
  141. package/src/cli/commands/completions.ts +0 -3
  142. package/src/cli/commands/credentials.ts +110 -23
  143. package/src/cli/commands/oauth/apps.ts +255 -0
  144. package/src/cli/commands/oauth/connections.ts +299 -0
  145. package/src/cli/commands/oauth/index.ts +52 -0
  146. package/src/cli/commands/oauth/providers.ts +242 -0
  147. package/src/cli/commands/skills.ts +4 -338
  148. package/src/cli/program.ts +1 -5
  149. package/src/cli/reference.ts +1 -3
  150. package/src/cli.ts +3 -2
  151. package/src/config/assistant-feature-flags.ts +0 -3
  152. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  153. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  154. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  155. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  156. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  157. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  158. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  159. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  160. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  161. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  162. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  163. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  164. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  165. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  166. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  167. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  168. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  169. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  170. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  171. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  172. package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
  173. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  174. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  175. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  176. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  177. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  178. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  179. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  180. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  181. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  182. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  183. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  184. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  185. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  186. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  187. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  188. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  189. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  190. package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
  191. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  192. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  193. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  194. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  195. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  196. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  197. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  198. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  199. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  200. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  201. package/src/config/env-registry.ts +14 -83
  202. package/src/config/env.ts +11 -50
  203. package/src/config/feature-flag-registry.json +16 -16
  204. package/src/config/schema.ts +3 -1
  205. package/src/config/skills.ts +21 -2
  206. package/src/context/image-dimensions.ts +229 -0
  207. package/src/context/token-estimator.ts +75 -12
  208. package/src/context/window-manager.ts +49 -10
  209. package/src/daemon/assistant-attachments.ts +1 -13
  210. package/src/daemon/guardian-action-generators.ts +4 -5
  211. package/src/daemon/handlers/config-ingress.ts +8 -33
  212. package/src/daemon/handlers/config-slack-channel.ts +76 -56
  213. package/src/daemon/handlers/config-telegram.ts +53 -24
  214. package/src/daemon/handlers/sessions.ts +10 -24
  215. package/src/daemon/handlers/shared.ts +0 -130
  216. package/src/daemon/host-cu-proxy.ts +401 -0
  217. package/src/daemon/lifecycle.ts +39 -63
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/computer-use.ts +2 -119
  220. package/src/daemon/message-types/host-cu.ts +19 -0
  221. package/src/daemon/message-types/integrations.ts +1 -0
  222. package/src/daemon/message-types/messages.ts +3 -0
  223. package/src/daemon/server.ts +14 -21
  224. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  225. package/src/daemon/session-attachments.ts +1 -2
  226. package/src/daemon/session-messaging.ts +3 -1
  227. package/src/daemon/session-slash.ts +1 -1
  228. package/src/daemon/session-surfaces.ts +40 -28
  229. package/src/daemon/session-tool-setup.ts +20 -11
  230. package/src/daemon/session.ts +139 -16
  231. package/src/daemon/tool-side-effects.ts +2 -8
  232. package/src/daemon/watch-handler.ts +2 -2
  233. package/src/email/providers/index.ts +2 -1
  234. package/src/events/tool-metrics-listener.ts +2 -2
  235. package/src/hooks/manager.ts +1 -4
  236. package/src/inbound/public-ingress-urls.ts +7 -7
  237. package/src/instrument.ts +15 -1
  238. package/src/logfire.ts +16 -5
  239. package/src/media/app-icon-generator.ts +30 -4
  240. package/src/media/avatar-router.ts +26 -3
  241. package/src/media/gemini-image-service.ts +28 -2
  242. package/src/memory/conversation-key-store.ts +21 -0
  243. package/src/memory/db-init.ts +4 -0
  244. package/src/memory/guardian-action-store.ts +1 -1
  245. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  246. package/src/memory/migrations/index.ts +1 -0
  247. package/src/memory/schema/guardian.ts +1 -1
  248. package/src/memory/schema/index.ts +1 -0
  249. package/src/memory/schema/oauth.ts +65 -0
  250. package/src/messaging/provider.ts +19 -13
  251. package/src/messaging/providers/gmail/adapter.ts +40 -23
  252. package/src/messaging/providers/gmail/client.ts +283 -122
  253. package/src/messaging/providers/gmail/people-client.ts +32 -24
  254. package/src/messaging/providers/slack/adapter.ts +29 -19
  255. package/src/messaging/providers/slack/client.ts +265 -78
  256. package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
  257. package/src/messaging/providers/whatsapp/adapter.ts +17 -11
  258. package/src/messaging/registry.ts +2 -31
  259. package/src/notifications/copy-composer.ts +0 -5
  260. package/src/notifications/signal.ts +4 -5
  261. package/src/oauth/byo-connection.test.ts +537 -0
  262. package/src/oauth/byo-connection.ts +128 -0
  263. package/src/oauth/connect-orchestrator.ts +139 -56
  264. package/src/oauth/connect-types.ts +17 -23
  265. package/src/oauth/connection-resolver.ts +58 -0
  266. package/src/oauth/connection.ts +38 -0
  267. package/src/oauth/manual-token-connection.ts +104 -0
  268. package/src/oauth/oauth-store.ts +496 -0
  269. package/src/oauth/platform-connection.test.ts +192 -0
  270. package/src/oauth/platform-connection.ts +111 -0
  271. package/src/oauth/provider-behaviors.ts +124 -0
  272. package/src/oauth/scope-policy.ts +9 -2
  273. package/src/oauth/seed-providers.ts +161 -0
  274. package/src/oauth/token-persistence.ts +74 -78
  275. package/src/permissions/checker.ts +8 -4
  276. package/src/permissions/defaults.ts +0 -1
  277. package/src/permissions/prompter.ts +10 -1
  278. package/src/permissions/trust-store.ts +13 -0
  279. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  280. package/src/prompts/system-prompt.ts +70 -45
  281. package/src/providers/anthropic/client.ts +133 -24
  282. package/src/providers/gemini/client.ts +15 -6
  283. package/src/providers/managed-proxy/constants.ts +2 -2
  284. package/src/providers/managed-proxy/context.ts +5 -1
  285. package/src/providers/ratelimit.ts +17 -0
  286. package/src/providers/registry.ts +2 -2
  287. package/src/providers/retry.ts +1 -27
  288. package/src/runtime/AGENTS.md +17 -0
  289. package/src/runtime/auth/route-policy.ts +0 -3
  290. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  291. package/src/runtime/channel-readiness-service.ts +168 -195
  292. package/src/runtime/channel-readiness-types.ts +4 -0
  293. package/src/runtime/channel-reply-delivery.ts +0 -40
  294. package/src/runtime/gateway-client.ts +0 -7
  295. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  296. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  297. package/src/runtime/guardian-action-message-composer.ts +3 -23
  298. package/src/runtime/http-server.ts +17 -10
  299. package/src/runtime/http-types.ts +2 -3
  300. package/src/runtime/middleware/rate-limiter.ts +74 -20
  301. package/src/runtime/middleware/twilio-validation.ts +1 -11
  302. package/src/runtime/pending-interactions.ts +14 -12
  303. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  304. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  305. package/src/runtime/routes/conversation-routes.ts +73 -19
  306. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  307. package/src/runtime/routes/events-routes.ts +21 -11
  308. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  309. package/src/runtime/routes/host-cu-routes.ts +97 -0
  310. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  311. package/src/runtime/routes/integrations/slack/share.ts +6 -6
  312. package/src/runtime/routes/integrations/twilio.ts +6 -5
  313. package/src/runtime/routes/log-export-routes.ts +126 -8
  314. package/src/runtime/routes/secret-routes.ts +3 -2
  315. package/src/runtime/routes/settings-routes.ts +113 -48
  316. package/src/runtime/routes/surface-action-routes.ts +1 -1
  317. package/src/runtime/routes/watch-routes.ts +128 -0
  318. package/src/schedule/integration-status.ts +10 -8
  319. package/src/security/credential-key.ts +14 -0
  320. package/src/security/keychain-broker-client.ts +5 -6
  321. package/src/security/oauth2.ts +1 -1
  322. package/src/security/token-manager.ts +145 -43
  323. package/src/skills/catalog-install.ts +358 -0
  324. package/src/skills/include-graph.ts +32 -0
  325. package/src/telegram/bot-username.ts +2 -3
  326. package/src/tools/apps/definitions.ts +0 -5
  327. package/src/tools/assets/materialize.ts +0 -5
  328. package/src/tools/assets/search.ts +0 -5
  329. package/src/tools/browser/headless-browser.ts +1 -67
  330. package/src/tools/browser/network-recorder.ts +1 -1
  331. package/src/tools/browser/network-recording-types.ts +1 -1
  332. package/src/tools/claude-code/claude-code.ts +0 -5
  333. package/src/tools/computer-use/definitions.ts +46 -11
  334. package/src/tools/computer-use/registry.ts +4 -5
  335. package/src/tools/credentials/broker.ts +5 -4
  336. package/src/tools/credentials/metadata-store.ts +22 -74
  337. package/src/tools/credentials/resolve.ts +2 -1
  338. package/src/tools/credentials/vault.ts +139 -151
  339. package/src/tools/filesystem/edit.ts +1 -6
  340. package/src/tools/filesystem/read.ts +0 -5
  341. package/src/tools/filesystem/write.ts +1 -6
  342. package/src/tools/host-filesystem/edit.ts +1 -6
  343. package/src/tools/host-filesystem/read.ts +1 -6
  344. package/src/tools/host-filesystem/write.ts +1 -6
  345. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  346. package/src/tools/memory/definitions.ts +0 -5
  347. package/src/tools/network/web-fetch.ts +0 -5
  348. package/src/tools/network/web-search.ts +0 -5
  349. package/src/tools/registry.ts +2 -7
  350. package/src/tools/schema-transforms.ts +99 -0
  351. package/src/tools/skills/load.ts +62 -8
  352. package/src/tools/swarm/delegate.ts +0 -5
  353. package/src/tools/system/avatar-generator.ts +0 -5
  354. package/src/tools/ui-surface/definitions.ts +0 -15
  355. package/src/tools/watch/screen-watch.ts +0 -5
  356. package/src/tools/watch/watch-state.ts +0 -12
  357. package/src/util/logger.ts +7 -41
  358. package/src/util/platform.ts +9 -28
  359. package/src/version.ts +10 -0
  360. package/src/watcher/providers/github.ts +51 -52
  361. package/src/watcher/providers/gmail.ts +88 -80
  362. package/src/watcher/providers/google-calendar.ts +94 -86
  363. package/src/watcher/providers/linear.ts +87 -93
  364. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  365. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  366. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  367. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  368. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  369. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  370. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  371. package/src/cli/commands/dev.ts +0 -129
  372. package/src/cli/commands/map.ts +0 -391
  373. package/src/cli/commands/oauth.ts +0 -77
  374. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  375. package/src/daemon/computer-use-session.ts +0 -1020
  376. package/src/daemon/ride-shotgun-handler.ts +0 -567
  377. package/src/oauth/provider-profiles.ts +0 -192
  378. package/src/prompts/computer-use-prompt.ts +0 -98
  379. package/src/runtime/routes/computer-use-routes.ts +0 -641
  380. package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
  381. package/src/runtime/telegram-streaming-delivery.ts +0 -383
  382. package/src/tools/computer-use/request-computer-control.ts +0 -61
@@ -0,0 +1,496 @@
1
+ /**
2
+ * CRUD store for OAuth providers, apps, and connections.
3
+ *
4
+ * Backed by Drizzle + SQLite. All JSON fields (default_scopes, scope_policy,
5
+ * extra_params, granted_scopes, metadata) are stored as serialized JSON strings.
6
+ */
7
+
8
+ import { and, desc, eq, sql } from "drizzle-orm";
9
+ import { v4 as uuid } from "uuid";
10
+
11
+ import { getDb, rawChanges } from "../memory/db.js";
12
+ import {
13
+ oauthApps,
14
+ oauthConnections,
15
+ oauthProviders,
16
+ } from "../memory/schema/oauth.js";
17
+ import {
18
+ deleteSecureKeyAsync,
19
+ getSecureKey,
20
+ setSecureKeyAsync,
21
+ } from "../security/secure-keys.js";
22
+ import { getLogger } from "../util/logger.js";
23
+
24
+ const log = getLogger("oauth-store");
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Row types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export type OAuthProviderRow = typeof oauthProviders.$inferSelect;
31
+ export type OAuthAppRow = typeof oauthApps.$inferSelect;
32
+ export type OAuthConnectionRow = typeof oauthConnections.$inferSelect;
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Provider operations
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Seed well-known provider profiles into the database. Uses INSERT … ON
40
+ * CONFLICT DO UPDATE so that corrections to seed data (e.g. a fixed baseUrl)
41
+ * propagate to existing installations on the next startup.
42
+ */
43
+ export function seedProviders(
44
+ profiles: Array<{
45
+ providerKey: string;
46
+ authUrl: string;
47
+ tokenUrl: string;
48
+ tokenEndpointAuthMethod?: string;
49
+ userinfoUrl?: string;
50
+ baseUrl?: string;
51
+ defaultScopes: string[];
52
+ scopePolicy: Record<string, unknown>;
53
+ extraParams?: Record<string, string>;
54
+ callbackTransport?: string;
55
+ loopbackPort?: number;
56
+ }>,
57
+ ): void {
58
+ const db = getDb();
59
+ const now = Date.now();
60
+ for (const p of profiles) {
61
+ const authUrl = p.authUrl;
62
+ const tokenUrl = p.tokenUrl;
63
+ const tokenEndpointAuthMethod = p.tokenEndpointAuthMethod ?? null;
64
+ const userinfoUrl = p.userinfoUrl ?? null;
65
+ const baseUrl = p.baseUrl ?? null;
66
+ const defaultScopes = JSON.stringify(p.defaultScopes);
67
+ const scopePolicy = JSON.stringify(p.scopePolicy);
68
+ const extraParams = p.extraParams ? JSON.stringify(p.extraParams) : null;
69
+ const callbackTransport = p.callbackTransport ?? null;
70
+ const loopbackPort = p.loopbackPort ?? null;
71
+
72
+ db.insert(oauthProviders)
73
+ .values({
74
+ providerKey: p.providerKey,
75
+ authUrl,
76
+ tokenUrl,
77
+ tokenEndpointAuthMethod,
78
+ userinfoUrl,
79
+ baseUrl,
80
+ defaultScopes,
81
+ scopePolicy,
82
+ extraParams,
83
+ callbackTransport,
84
+ loopbackPort,
85
+ createdAt: now,
86
+ updatedAt: now,
87
+ })
88
+ .onConflictDoUpdate({
89
+ target: oauthProviders.providerKey,
90
+ set: {
91
+ authUrl,
92
+ tokenUrl,
93
+ tokenEndpointAuthMethod,
94
+ userinfoUrl,
95
+ baseUrl,
96
+ defaultScopes,
97
+ scopePolicy,
98
+ extraParams,
99
+ callbackTransport,
100
+ loopbackPort,
101
+ updatedAt: now,
102
+ },
103
+ })
104
+ .run();
105
+ }
106
+ }
107
+
108
+ /** Look up a provider by its primary key. */
109
+ export function getProvider(providerKey: string): OAuthProviderRow | undefined {
110
+ const db = getDb();
111
+ return db
112
+ .select()
113
+ .from(oauthProviders)
114
+ .where(eq(oauthProviders.providerKey, providerKey))
115
+ .get();
116
+ }
117
+
118
+ /** Return all registered providers. */
119
+ export function listProviders(): OAuthProviderRow[] {
120
+ const db = getDb();
121
+ return db.select().from(oauthProviders).all();
122
+ }
123
+
124
+ /**
125
+ * Register a new provider (for dynamic registration). Throws if the
126
+ * provider_key already exists.
127
+ */
128
+ export function registerProvider(params: {
129
+ providerKey: string;
130
+ authUrl: string;
131
+ tokenUrl: string;
132
+ tokenEndpointAuthMethod?: string;
133
+ userinfoUrl?: string;
134
+ baseUrl?: string;
135
+ defaultScopes: string[];
136
+ scopePolicy: Record<string, unknown>;
137
+ extraParams?: Record<string, string>;
138
+ callbackTransport?: string;
139
+ loopbackPort?: number;
140
+ }): OAuthProviderRow {
141
+ const db = getDb();
142
+ const now = Date.now();
143
+
144
+ const existing = getProvider(params.providerKey);
145
+ if (existing) {
146
+ throw new Error(`OAuth provider already exists: ${params.providerKey}`);
147
+ }
148
+
149
+ const row = {
150
+ providerKey: params.providerKey,
151
+ authUrl: params.authUrl,
152
+ tokenUrl: params.tokenUrl,
153
+ tokenEndpointAuthMethod: params.tokenEndpointAuthMethod ?? null,
154
+ userinfoUrl: params.userinfoUrl ?? null,
155
+ baseUrl: params.baseUrl ?? null,
156
+ defaultScopes: JSON.stringify(params.defaultScopes),
157
+ scopePolicy: JSON.stringify(params.scopePolicy),
158
+ extraParams: params.extraParams ? JSON.stringify(params.extraParams) : null,
159
+ callbackTransport: params.callbackTransport ?? null,
160
+ loopbackPort: params.loopbackPort ?? null,
161
+ createdAt: now,
162
+ updatedAt: now,
163
+ };
164
+
165
+ db.insert(oauthProviders).values(row).run();
166
+
167
+ return row;
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // App operations
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * Insert or return an existing app by (provider_key, client_id).
176
+ * Generates a UUID on insert.
177
+ */
178
+ export async function upsertApp(
179
+ providerKey: string,
180
+ clientId: string,
181
+ clientSecret?: string,
182
+ ): Promise<OAuthAppRow> {
183
+ const db = getDb();
184
+
185
+ const existing = db
186
+ .select()
187
+ .from(oauthApps)
188
+ .where(
189
+ and(
190
+ eq(oauthApps.providerKey, providerKey),
191
+ eq(oauthApps.clientId, clientId),
192
+ ),
193
+ )
194
+ .get();
195
+
196
+ if (existing) {
197
+ if (clientSecret) {
198
+ const stored = await setSecureKeyAsync(
199
+ `oauth_app/${existing.id}/client_secret`,
200
+ clientSecret,
201
+ );
202
+ if (!stored) {
203
+ throw new Error("Failed to store client_secret in secure storage");
204
+ }
205
+ }
206
+ return existing;
207
+ }
208
+
209
+ const now = Date.now();
210
+ const id = uuid();
211
+ const row = {
212
+ id,
213
+ providerKey,
214
+ clientId,
215
+ createdAt: now,
216
+ updatedAt: now,
217
+ };
218
+
219
+ db.insert(oauthApps).values(row).run();
220
+
221
+ if (clientSecret) {
222
+ const stored = await setSecureKeyAsync(
223
+ `oauth_app/${id}/client_secret`,
224
+ clientSecret,
225
+ );
226
+ if (!stored) {
227
+ throw new Error("Failed to store client_secret in secure storage");
228
+ }
229
+ }
230
+
231
+ return row;
232
+ }
233
+
234
+ /** Look up an app by its primary key. */
235
+ export function getApp(id: string): OAuthAppRow | undefined {
236
+ const db = getDb();
237
+ return db.select().from(oauthApps).where(eq(oauthApps.id, id)).get();
238
+ }
239
+
240
+ /** Look up an app by (provider_key, client_id). */
241
+ export function getAppByProviderAndClientId(
242
+ providerKey: string,
243
+ clientId: string,
244
+ ): OAuthAppRow | undefined {
245
+ const db = getDb();
246
+ return db
247
+ .select()
248
+ .from(oauthApps)
249
+ .where(
250
+ and(
251
+ eq(oauthApps.providerKey, providerKey),
252
+ eq(oauthApps.clientId, clientId),
253
+ ),
254
+ )
255
+ .get();
256
+ }
257
+
258
+ /**
259
+ * Get the most recently created app for a provider.
260
+ * Returns undefined if no app exists for this provider.
261
+ */
262
+ export function getMostRecentAppByProvider(
263
+ providerKey: string,
264
+ ): OAuthAppRow | undefined {
265
+ const db = getDb();
266
+ return db
267
+ .select()
268
+ .from(oauthApps)
269
+ .where(eq(oauthApps.providerKey, providerKey))
270
+ .orderBy(desc(oauthApps.createdAt))
271
+ .limit(1)
272
+ .get();
273
+ }
274
+
275
+ /** Return all OAuth apps. */
276
+ export function listApps(): OAuthAppRow[] {
277
+ const db = getDb();
278
+ return db.select().from(oauthApps).all();
279
+ }
280
+
281
+ /** Delete an app by ID. Cleans up the client_secret from secure storage. Returns true if a row was deleted. */
282
+ export async function deleteApp(id: string): Promise<boolean> {
283
+ // Delete the DB row first so that if it fails (e.g. FK constraint from
284
+ // existing connections), the secret in secure storage remains intact.
285
+ const db = getDb();
286
+ db.delete(oauthApps).where(eq(oauthApps.id, id)).run();
287
+ const deleted = rawChanges() > 0;
288
+
289
+ if (!deleted) return false;
290
+
291
+ const result = await deleteSecureKeyAsync(`oauth_app/${id}/client_secret`);
292
+ if (result === "error") {
293
+ throw new Error(
294
+ `Deleted app ${id} but failed to remove client_secret from secure storage`,
295
+ );
296
+ }
297
+
298
+ return true;
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Connection operations
303
+ // ---------------------------------------------------------------------------
304
+
305
+ /**
306
+ * Create a new OAuth connection. Generates a UUID and sets status='active'.
307
+ * `metadata` is an optional JSON object for provider-specific token response data.
308
+ */
309
+ export function createConnection(params: {
310
+ oauthAppId: string;
311
+ providerKey: string;
312
+ accountInfo?: string;
313
+ grantedScopes: string[];
314
+ expiresAt?: number;
315
+ hasRefreshToken: boolean;
316
+ label?: string;
317
+ metadata?: Record<string, unknown>;
318
+ /** Override the creation timestamp. Useful in tests to ensure deterministic ordering. */
319
+ createdAt?: number;
320
+ }): OAuthConnectionRow {
321
+ const db = getDb();
322
+ const now = params.createdAt ?? Date.now();
323
+ const id = uuid();
324
+
325
+ const row = {
326
+ id,
327
+ oauthAppId: params.oauthAppId,
328
+ providerKey: params.providerKey,
329
+ accountInfo: params.accountInfo ?? null,
330
+ grantedScopes: JSON.stringify(params.grantedScopes),
331
+ expiresAt: params.expiresAt ?? null,
332
+ hasRefreshToken: params.hasRefreshToken ? 1 : 0,
333
+ status: "active" as const,
334
+ label: params.label ?? null,
335
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
336
+ createdAt: now,
337
+ updatedAt: now,
338
+ };
339
+
340
+ db.insert(oauthConnections).values(row).run();
341
+
342
+ return row;
343
+ }
344
+
345
+ /** Look up a connection by its primary key. */
346
+ export function getConnection(id: string): OAuthConnectionRow | undefined {
347
+ const db = getDb();
348
+ return db
349
+ .select()
350
+ .from(oauthConnections)
351
+ .where(eq(oauthConnections.id, id))
352
+ .get();
353
+ }
354
+
355
+ /**
356
+ * Get the most recent active connection for a provider.
357
+ * Returns undefined if no active connection exists.
358
+ */
359
+ export function getConnectionByProvider(
360
+ providerKey: string,
361
+ ): OAuthConnectionRow | undefined {
362
+ const db = getDb();
363
+ return db
364
+ .select()
365
+ .from(oauthConnections)
366
+ .where(
367
+ and(
368
+ eq(oauthConnections.providerKey, providerKey),
369
+ eq(oauthConnections.status, "active"),
370
+ ),
371
+ )
372
+ .orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
373
+ .limit(1)
374
+ .get();
375
+ }
376
+
377
+ /**
378
+ * Check whether a provider has a usable OAuth connection: an active row in the
379
+ * database AND a corresponding access token in secure storage.
380
+ *
381
+ * This guards against the edge case where the connection row was created/updated
382
+ * but the secure-key write for the access token failed, which would make
383
+ * `resolveOAuthConnection()` throw at usage time.
384
+ */
385
+ export function isProviderConnected(providerKey: string): boolean {
386
+ const conn = getConnectionByProvider(providerKey);
387
+ if (!conn || conn.status !== "active") return false;
388
+ return getSecureKey(`oauth_connection/${conn.id}/access_token`) !== undefined;
389
+ }
390
+
391
+ /**
392
+ * Update fields on an existing connection. Returns true if a row was updated.
393
+ */
394
+ export function updateConnection(
395
+ id: string,
396
+ updates: Partial<{
397
+ oauthAppId: string;
398
+ accountInfo: string;
399
+ grantedScopes: string[];
400
+ /** Pass `null` to explicitly clear a stale expiresAt in the DB. */
401
+ expiresAt: number | null;
402
+ hasRefreshToken: boolean;
403
+ status: string;
404
+ label: string;
405
+ metadata: Record<string, unknown>;
406
+ }>,
407
+ ): boolean {
408
+ const db = getDb();
409
+ const now = Date.now();
410
+
411
+ // Build the set clause, serializing JSON fields and converting booleans.
412
+ // For expiresAt, null means "clear the column" so we check for undefined
413
+ // explicitly rather than truthiness.
414
+ const set: Record<string, unknown> = { updatedAt: now };
415
+ if (updates.oauthAppId !== undefined) set.oauthAppId = updates.oauthAppId;
416
+ if (updates.accountInfo !== undefined) set.accountInfo = updates.accountInfo;
417
+ if (updates.grantedScopes !== undefined)
418
+ set.grantedScopes = JSON.stringify(updates.grantedScopes);
419
+ if (updates.expiresAt !== undefined) set.expiresAt = updates.expiresAt;
420
+ if (updates.hasRefreshToken !== undefined)
421
+ set.hasRefreshToken = updates.hasRefreshToken ? 1 : 0;
422
+ if (updates.status !== undefined) set.status = updates.status;
423
+ if (updates.label !== undefined) set.label = updates.label;
424
+ if (updates.metadata !== undefined)
425
+ set.metadata = JSON.stringify(updates.metadata);
426
+
427
+ db.update(oauthConnections).set(set).where(eq(oauthConnections.id, id)).run();
428
+
429
+ return rawChanges() > 0;
430
+ }
431
+
432
+ /** List connections, optionally filtered by provider key. */
433
+ export function listConnections(providerKey?: string): OAuthConnectionRow[] {
434
+ const db = getDb();
435
+
436
+ if (providerKey) {
437
+ return db
438
+ .select()
439
+ .from(oauthConnections)
440
+ .where(eq(oauthConnections.providerKey, providerKey))
441
+ .all();
442
+ }
443
+
444
+ return db.select().from(oauthConnections).all();
445
+ }
446
+
447
+ /** Delete a connection by ID. Returns true if a row was deleted. */
448
+ export function deleteConnection(id: string): boolean {
449
+ const db = getDb();
450
+ db.delete(oauthConnections).where(eq(oauthConnections.id, id)).run();
451
+ return rawChanges() > 0;
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Disconnect (full cleanup)
456
+ // ---------------------------------------------------------------------------
457
+
458
+ /**
459
+ * Fully disconnect an OAuth provider: delete the new-format secure keys
460
+ * (access_token and refresh_token) and remove the connection row from SQLite.
461
+ *
462
+ * Returns `"disconnected"` if a connection was found and cleaned up,
463
+ * `"not-found"` if no active connection existed for the given provider,
464
+ * or `"error"` if secure key deletion failed (connection row is preserved
465
+ * to avoid orphaning secrets).
466
+ */
467
+ export async function disconnectOAuthProvider(
468
+ providerKey: string,
469
+ ): Promise<"disconnected" | "not-found" | "error"> {
470
+ const conn = getConnectionByProvider(providerKey);
471
+ if (!conn) return "not-found";
472
+
473
+ const r1 = await deleteSecureKeyAsync(
474
+ `oauth_connection/${conn.id}/access_token`,
475
+ );
476
+ const r2 = await deleteSecureKeyAsync(
477
+ `oauth_connection/${conn.id}/refresh_token`,
478
+ );
479
+
480
+ if (r1 === "error" || r2 === "error") {
481
+ log.warn(
482
+ {
483
+ providerKey,
484
+ connectionId: conn.id,
485
+ accessTokenResult: r1,
486
+ refreshTokenResult: r2,
487
+ },
488
+ "Failed to delete OAuth secure keys — skipping connection row deletion to avoid orphaning secrets",
489
+ );
490
+ return "error";
491
+ }
492
+
493
+ deleteConnection(conn.id);
494
+
495
+ return "disconnected";
496
+ }
@@ -0,0 +1,192 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { BackendError, VellumError } from "../util/errors.js";
4
+ import {
5
+ CredentialRequiredError,
6
+ PlatformOAuthConnection,
7
+ ProviderUnreachableError,
8
+ } from "./platform-connection.js";
9
+
10
+ const DEFAULT_OPTIONS = {
11
+ id: "conn-1",
12
+ providerKey: "integration:gmail",
13
+ externalId: "ext-123",
14
+ accountInfo: "user@example.com",
15
+ grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
16
+ assistantId: "asst-abc",
17
+ platformBaseUrl: "https://platform.example.com",
18
+ apiKey: "test-api-key",
19
+ };
20
+
21
+ describe("PlatformOAuthConnection", () => {
22
+ let originalFetch: typeof globalThis.fetch;
23
+
24
+ beforeEach(() => {
25
+ originalFetch = globalThis.fetch;
26
+ });
27
+
28
+ afterEach(() => {
29
+ globalThis.fetch = originalFetch;
30
+ });
31
+
32
+ test("successful proxied request", async () => {
33
+ const upstreamBody = { messages: [{ id: "msg-1", snippet: "Hello" }] };
34
+
35
+ globalThis.fetch = mock(
36
+ async (url: string | URL | Request, init?: RequestInit) => {
37
+ expect(url).toBe(
38
+ "https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/gmail/",
39
+ );
40
+ expect(init?.method).toBe("POST");
41
+ expect(init?.headers).toEqual({
42
+ Authorization: "Api-Key test-api-key",
43
+ "Content-Type": "application/json",
44
+ });
45
+
46
+ const parsed = JSON.parse(init?.body as string);
47
+ expect(parsed).toEqual({
48
+ request: {
49
+ method: "GET",
50
+ path: "/gmail/v1/users/me/messages",
51
+ query: { maxResults: "10" },
52
+ headers: {},
53
+ body: null,
54
+ },
55
+ });
56
+
57
+ return new Response(
58
+ JSON.stringify({
59
+ status: 200,
60
+ headers: { "content-type": "application/json" },
61
+ body: upstreamBody,
62
+ }),
63
+ { status: 200 },
64
+ );
65
+ },
66
+ ) as unknown as typeof globalThis.fetch;
67
+
68
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
69
+ const result = await conn.request({
70
+ method: "GET",
71
+ path: "/gmail/v1/users/me/messages",
72
+ query: { maxResults: "10" },
73
+ });
74
+
75
+ expect(result.status).toBe(200);
76
+ expect(result.headers).toEqual({ "content-type": "application/json" });
77
+ expect(result.body).toEqual(upstreamBody);
78
+ });
79
+
80
+ test("forwards baseUrl when provided", async () => {
81
+ globalThis.fetch = mock(
82
+ async (_url: string | URL | Request, init?: RequestInit) => {
83
+ const parsed = JSON.parse(init?.body as string);
84
+ expect(parsed.request.baseUrl).toBe(
85
+ "https://www.googleapis.com/calendar/v3",
86
+ );
87
+
88
+ return new Response(
89
+ JSON.stringify({ status: 200, headers: {}, body: {} }),
90
+ { status: 200 },
91
+ );
92
+ },
93
+ ) as unknown as typeof globalThis.fetch;
94
+
95
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
96
+ await conn.request({
97
+ method: "GET",
98
+ path: "/calendars/primary/events",
99
+ baseUrl: "https://www.googleapis.com/calendar/v3",
100
+ });
101
+ });
102
+
103
+ test("omits baseUrl from envelope when not provided", async () => {
104
+ globalThis.fetch = mock(
105
+ async (_url: string | URL | Request, init?: RequestInit) => {
106
+ const parsed = JSON.parse(init?.body as string);
107
+ expect("baseUrl" in parsed.request).toBe(false);
108
+
109
+ return new Response(
110
+ JSON.stringify({ status: 200, headers: {}, body: null }),
111
+ { status: 200 },
112
+ );
113
+ },
114
+ ) as unknown as typeof globalThis.fetch;
115
+
116
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
117
+ await conn.request({ method: "GET", path: "/some/path" });
118
+ });
119
+
120
+ test("error classes extend VellumError hierarchy", () => {
121
+ const credErr = new CredentialRequiredError();
122
+ expect(credErr).toBeInstanceOf(BackendError);
123
+ expect(credErr).toBeInstanceOf(VellumError);
124
+
125
+ const provErr = new ProviderUnreachableError();
126
+ expect(provErr).toBeInstanceOf(BackendError);
127
+ expect(provErr).toBeInstanceOf(VellumError);
128
+ });
129
+
130
+ test("424 response throws CredentialRequiredError", async () => {
131
+ globalThis.fetch = mock(async () => {
132
+ return new Response("", { status: 424 });
133
+ }) as unknown as typeof globalThis.fetch;
134
+
135
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
136
+ await expect(
137
+ conn.request({ method: "GET", path: "/test" }),
138
+ ).rejects.toThrow(CredentialRequiredError);
139
+ });
140
+
141
+ test("502 response throws ProviderUnreachableError", async () => {
142
+ globalThis.fetch = mock(async () => {
143
+ return new Response("", { status: 502 });
144
+ }) as unknown as typeof globalThis.fetch;
145
+
146
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
147
+ await expect(
148
+ conn.request({ method: "GET", path: "/test" }),
149
+ ).rejects.toThrow(ProviderUnreachableError);
150
+ });
151
+
152
+ test("withToken throws clear error", async () => {
153
+ const conn = new PlatformOAuthConnection(DEFAULT_OPTIONS);
154
+ await expect(conn.withToken(async (token) => token)).rejects.toThrow(
155
+ "Raw token access is not supported for platform-managed connections. Use connection.request() instead.",
156
+ );
157
+ });
158
+
159
+ test("strips trailing slash from platformBaseUrl to avoid double slashes", async () => {
160
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
161
+ expect(String(url)).toBe(
162
+ "https://platform.example.com/v1/assistants/asst-abc/external-provider-proxy/gmail/",
163
+ );
164
+ return new Response(
165
+ JSON.stringify({ status: 200, headers: {}, body: null }),
166
+ { status: 200 },
167
+ );
168
+ }) as unknown as typeof globalThis.fetch;
169
+
170
+ const conn = new PlatformOAuthConnection({
171
+ ...DEFAULT_OPTIONS,
172
+ platformBaseUrl: "https://platform.example.com/",
173
+ });
174
+ await conn.request({ method: "GET", path: "/test" });
175
+ });
176
+
177
+ test("strips integration: prefix from providerKey for slug", async () => {
178
+ globalThis.fetch = mock(async (url: string | URL | Request) => {
179
+ expect(String(url)).toContain("/external-provider-proxy/slack/");
180
+ return new Response(
181
+ JSON.stringify({ status: 200, headers: {}, body: null }),
182
+ { status: 200 },
183
+ );
184
+ }) as unknown as typeof globalThis.fetch;
185
+
186
+ const conn = new PlatformOAuthConnection({
187
+ ...DEFAULT_OPTIONS,
188
+ providerKey: "integration:slack",
189
+ });
190
+ await conn.request({ method: "GET", path: "/test" });
191
+ });
192
+ });