@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.2

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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -0,0 +1,360 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // --- Mutable mock state (per test) -----------------------------------------
4
+ let mockWebFetchProvider: string | undefined = "default";
5
+ let mockFirecrawlSecureKey: string | undefined;
6
+
7
+ // Capture the registered tool so we can exercise provider dispatch.
8
+ let capturedTool: any = null;
9
+
10
+ mock.module("../../registry.js", () => ({
11
+ registerTool: (tool: any) => {
12
+ capturedTool = tool;
13
+ },
14
+ }));
15
+
16
+ mock.module("../../../config/loader.js", () => ({
17
+ getConfig: () => ({
18
+ services: {
19
+ "web-fetch": { mode: "your-own", provider: mockWebFetchProvider },
20
+ },
21
+ }),
22
+ }));
23
+
24
+ mock.module("../../../security/secure-keys.js", () => ({
25
+ getProviderKeyAsync: async (provider: string) =>
26
+ provider === "firecrawl" ? mockFirecrawlSecureKey : undefined,
27
+ }));
28
+
29
+ const realLogger = await import("../../../util/logger.js");
30
+ mock.module("../../../util/logger.js", () => ({
31
+ ...realLogger,
32
+ getLogger: () =>
33
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
34
+ }));
35
+
36
+ mock.module("../../../permissions/types.js", () => ({
37
+ RiskLevel: { Low: "low", Medium: "medium", High: "high" },
38
+ }));
39
+
40
+ // Keep real url-safety helpers (parseUrl, sanitize*, isPrivateOrLocalHost,
41
+ // resolveRequestAddress) but stub DNS resolution. The returned address list is
42
+ // mutable per test: an empty list makes the built-in fallback path
43
+ // short-circuit ("Unable to resolve host") before any socket is opened, while a
44
+ // public address lets a Firecrawl-routed request through the dispatcher's DNS
45
+ // safety gate. A private address exercises the "don't leak to Firecrawl" guard.
46
+ let mockResolveAddresses: string[] = [];
47
+ const realUrlSafety = await import("../url-safety.js");
48
+ mock.module("../url-safety.js", () => ({
49
+ ...realUrlSafety,
50
+ resolveHostAddresses: async () => mockResolveAddresses,
51
+ }));
52
+
53
+ const { executeFirecrawlScrape } = await import("../web-fetch.js");
54
+
55
+ const SCRAPE_URL = "api.firecrawl.dev/v2/scrape";
56
+
57
+ function scrapeResponse(body: unknown, status = 200): Response {
58
+ return new Response(JSON.stringify(body), {
59
+ status,
60
+ headers: { "content-type": "application/json" },
61
+ });
62
+ }
63
+
64
+ describe("executeFirecrawlScrape", () => {
65
+ let originalFetch: typeof globalThis.fetch;
66
+
67
+ beforeEach(() => {
68
+ originalFetch = globalThis.fetch;
69
+ });
70
+ afterEach(() => {
71
+ globalThis.fetch = originalFetch;
72
+ });
73
+
74
+ test("returns markdown content + metadata on success", async () => {
75
+ globalThis.fetch = (async () =>
76
+ scrapeResponse({
77
+ success: true,
78
+ data: {
79
+ markdown: "# Example\n\nHello from Firecrawl.",
80
+ metadata: {
81
+ title: "Example Domain",
82
+ description: "An example page",
83
+ url: "https://example.com/",
84
+ statusCode: 200,
85
+ contentType: "text/html",
86
+ },
87
+ },
88
+ })) as any;
89
+
90
+ const result = await executeFirecrawlScrape(
91
+ { url: "https://example.com" },
92
+ { apiKey: "fc-test-key" },
93
+ );
94
+
95
+ expect(result.isError).toBe(false);
96
+ expect(result.content).toContain("Hello from Firecrawl.");
97
+ expect(result.content).toContain("Mode: markdown");
98
+ expect(result.content).toContain("Title: Example Domain");
99
+ const meta = result.activityMetadata?.webFetch;
100
+ expect(meta?.provider).toBe("firecrawl");
101
+ expect(meta?.status).toBe(200);
102
+ expect(meta?.title).toBe("Example Domain");
103
+ expect(meta?.finalUrl).toContain("example.com");
104
+ });
105
+
106
+ test("sends the correct request shape", async () => {
107
+ let capturedUrl = "";
108
+ let capturedBody: any = null;
109
+ let capturedHeaders: Headers | null = null;
110
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
111
+ capturedUrl = url;
112
+ capturedBody = JSON.parse(init?.body as string);
113
+ capturedHeaders = new Headers(init?.headers);
114
+ return scrapeResponse({ success: true, data: { markdown: "ok" } });
115
+ }) as any;
116
+
117
+ await executeFirecrawlScrape(
118
+ { url: "https://example.com/docs" },
119
+ { apiKey: "fc-test-key" },
120
+ );
121
+
122
+ expect(capturedUrl).toContain(SCRAPE_URL);
123
+ expect(capturedBody.url).toBe("https://example.com/docs");
124
+ expect(capturedBody.formats).toEqual(["markdown"]);
125
+ expect(capturedBody.onlyMainContent).toBe(true);
126
+ expect(capturedHeaders!.get("authorization")).toBe("Bearer fc-test-key");
127
+ expect(capturedHeaders!.get("x-client-source")).toBe("vellum-assistant");
128
+ });
129
+
130
+ test("honors max_chars windowing and emits a truncation notice", async () => {
131
+ globalThis.fetch = (async () =>
132
+ scrapeResponse({
133
+ success: true,
134
+ data: { markdown: "abcdefghij", metadata: { statusCode: 200 } },
135
+ })) as any;
136
+
137
+ const result = await executeFirecrawlScrape(
138
+ { url: "https://example.com", max_chars: 4 },
139
+ { apiKey: "fc-key" },
140
+ );
141
+ expect(result.isError).toBe(false);
142
+ expect(result.activityMetadata?.webFetch?.truncated).toBe(true);
143
+ expect(result.status).toContain("truncated");
144
+ });
145
+
146
+ test("empty markdown yields a no-content marker, not an error", async () => {
147
+ globalThis.fetch = (async () =>
148
+ scrapeResponse({ success: true, data: { markdown: "" } })) as any;
149
+
150
+ const result = await executeFirecrawlScrape(
151
+ { url: "https://example.com" },
152
+ { apiKey: "fc-key" },
153
+ );
154
+ expect(result.isError).toBe(false);
155
+ expect(result.content).toContain("<no_content />");
156
+ });
157
+
158
+ test("rejects URLs with embedded credentials instead of forwarding them", async () => {
159
+ let hit = false;
160
+ globalThis.fetch = (async () => {
161
+ hit = true;
162
+ return scrapeResponse({ success: true, data: { markdown: "x" } });
163
+ }) as any;
164
+
165
+ const result = await executeFirecrawlScrape(
166
+ { url: "https://user:secret@example.com/page" },
167
+ { apiKey: "fc-key" },
168
+ );
169
+ expect(hit).toBe(false); // never sent to Firecrawl
170
+ expect(result.isError).toBe(true);
171
+ expect(result.content).toContain("embedded credentials");
172
+ // The secret must not leak into the surfaced url/metadata.
173
+ expect(result.content).not.toContain("secret");
174
+ });
175
+
176
+ test("surfaces a payload-level failure on a 200 (success:false / error)", async () => {
177
+ globalThis.fetch = (async () =>
178
+ scrapeResponse({
179
+ success: false,
180
+ error: "This website is no longer supported",
181
+ data: {},
182
+ })) as any;
183
+
184
+ const result = await executeFirecrawlScrape(
185
+ { url: "https://example.com" },
186
+ { apiKey: "fc-key" },
187
+ );
188
+ expect(result.isError).toBe(true);
189
+ expect(result.content).toContain("no longer supported");
190
+ });
191
+
192
+ test("surfaces invalid JSON as a clean error", async () => {
193
+ globalThis.fetch = (async () =>
194
+ new Response("<html>not json</html>", {
195
+ status: 200,
196
+ headers: { "content-type": "application/json" },
197
+ })) as any;
198
+
199
+ const result = await executeFirecrawlScrape(
200
+ { url: "https://example.com" },
201
+ { apiKey: "fc-key" },
202
+ );
203
+ expect(result.isError).toBe(true);
204
+ expect(result.content).toContain("invalid JSON");
205
+ });
206
+
207
+ test.each([401, 403])("surfaces %d as an invalid-key error", async (status) => {
208
+ globalThis.fetch = (async () =>
209
+ new Response("Unauthorized", { status })) as any;
210
+
211
+ const result = await executeFirecrawlScrape(
212
+ { url: "https://example.com" },
213
+ { apiKey: "bad-key" },
214
+ );
215
+ expect(result.isError).toBe(true);
216
+ expect(result.content).toContain("Invalid or expired Firecrawl API key");
217
+ expect(result.activityMetadata?.webFetch?.provider).toBe("firecrawl");
218
+ });
219
+
220
+ test("retries 429 then surfaces a rate-limit error", async () => {
221
+ let callCount = 0;
222
+ globalThis.fetch = (async () => {
223
+ callCount++;
224
+ return new Response("Too Many Requests", {
225
+ status: 429,
226
+ headers: { "retry-after": "0" },
227
+ });
228
+ }) as any;
229
+
230
+ const result = await executeFirecrawlScrape(
231
+ { url: "https://example.com" },
232
+ { apiKey: "fc-key" },
233
+ );
234
+ expect(result.isError).toBe(true);
235
+ expect(result.content).toContain("rate limit");
236
+ expect(callCount).toBe(4); // 1 + DEFAULT_MAX_RETRIES
237
+ });
238
+ });
239
+
240
+ describe("web_fetch provider dispatch", () => {
241
+ let originalFetch: typeof globalThis.fetch;
242
+
243
+ const execute = (input: Record<string, unknown>, ctx: any = {}) =>
244
+ capturedTool.execute(input, ctx);
245
+
246
+ beforeEach(() => {
247
+ originalFetch = globalThis.fetch;
248
+ mockWebFetchProvider = "default";
249
+ mockFirecrawlSecureKey = undefined;
250
+ mockResolveAddresses = [];
251
+ });
252
+ afterEach(() => {
253
+ globalThis.fetch = originalFetch;
254
+ });
255
+
256
+ test("routes to Firecrawl when provider=firecrawl and a key is set", async () => {
257
+ mockWebFetchProvider = "firecrawl";
258
+ mockFirecrawlSecureKey = "fc-key";
259
+ mockResolveAddresses = ["93.184.216.34"]; // public → passes the DNS safety gate
260
+ let hitUrl = "";
261
+ globalThis.fetch = (async (url: string) => {
262
+ hitUrl = url;
263
+ return scrapeResponse({
264
+ success: true,
265
+ data: { markdown: "routed", metadata: { statusCode: 200 } },
266
+ });
267
+ }) as any;
268
+
269
+ const result = await execute({ url: "https://example.com" });
270
+ expect(hitUrl).toContain(SCRAPE_URL);
271
+ expect(result.activityMetadata?.webFetch?.provider).toBe("firecrawl");
272
+ });
273
+
274
+ test("falls back to the built-in fetcher when provider=firecrawl but no key", async () => {
275
+ mockWebFetchProvider = "firecrawl";
276
+ mockFirecrawlSecureKey = undefined;
277
+ let firecrawlHit = false;
278
+ globalThis.fetch = (async (url: string) => {
279
+ if (typeof url === "string" && url.includes(SCRAPE_URL)) {
280
+ firecrawlHit = true;
281
+ }
282
+ return new Response("", { status: 200 });
283
+ }) as any;
284
+
285
+ const result = await execute({ url: "https://example.com" });
286
+ // Built-in path runs (DNS stubbed to empty → resolve error), Firecrawl not hit.
287
+ expect(firecrawlHit).toBe(false);
288
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
289
+ });
290
+
291
+ test("provider=default never touches Firecrawl", async () => {
292
+ mockWebFetchProvider = "default";
293
+ mockFirecrawlSecureKey = "fc-key";
294
+ let firecrawlHit = false;
295
+ globalThis.fetch = (async (url: string) => {
296
+ if (typeof url === "string" && url.includes(SCRAPE_URL)) {
297
+ firecrawlHit = true;
298
+ }
299
+ return new Response("", { status: 200 });
300
+ }) as any;
301
+
302
+ const result = await execute({ url: "https://example.com" });
303
+ expect(firecrawlHit).toBe(false);
304
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
305
+ });
306
+
307
+ test("private/local targets bypass Firecrawl and use the built-in fetcher", async () => {
308
+ mockWebFetchProvider = "firecrawl";
309
+ mockFirecrawlSecureKey = "fc-key";
310
+ let firecrawlHit = false;
311
+ globalThis.fetch = (async (url: string) => {
312
+ if (typeof url === "string" && url.includes(SCRAPE_URL)) {
313
+ firecrawlHit = true;
314
+ }
315
+ return new Response("", { status: 200 });
316
+ }) as any;
317
+
318
+ const result = await execute({ url: "http://localhost:8080/admin" });
319
+ expect(firecrawlHit).toBe(false);
320
+ expect(result.isError).toBe(true);
321
+ expect(result.content.toLowerCase()).toContain("private");
322
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
323
+ });
324
+
325
+ test("a public host that DNS-resolves to a private IP is not sent to Firecrawl", async () => {
326
+ mockWebFetchProvider = "firecrawl";
327
+ mockFirecrawlSecureKey = "fc-key";
328
+ mockResolveAddresses = ["10.0.0.5"]; // public name, private address → blocked
329
+ let firecrawlHit = false;
330
+ globalThis.fetch = (async (url: string) => {
331
+ if (typeof url === "string" && url.includes(SCRAPE_URL)) {
332
+ firecrawlHit = true;
333
+ }
334
+ return new Response("", { status: 200 });
335
+ }) as any;
336
+
337
+ const result = await execute({ url: "https://internal.example/secret?token=abc" });
338
+ expect(firecrawlHit).toBe(false); // internal URL never leaked to Firecrawl
339
+ expect(result.isError).toBe(true);
340
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
341
+ });
342
+
343
+ test("non-http(s) schemes are not sent to Firecrawl", async () => {
344
+ mockWebFetchProvider = "firecrawl";
345
+ mockFirecrawlSecureKey = "fc-key";
346
+ mockResolveAddresses = ["93.184.216.34"];
347
+ let firecrawlHit = false;
348
+ globalThis.fetch = (async (url: string) => {
349
+ if (typeof url === "string" && url.includes(SCRAPE_URL)) {
350
+ firecrawlHit = true;
351
+ }
352
+ return new Response("", { status: 200 });
353
+ }) as any;
354
+
355
+ const result = await execute({ url: "ftp://example.com/file" });
356
+ expect(firecrawlHit).toBe(false);
357
+ expect(result.isError).toBe(true);
358
+ expect(result.activityMetadata?.webFetch?.provider).toBe("default");
359
+ });
360
+ });
@@ -8,6 +8,7 @@ let mockWebSearchMode: string | undefined = "your-own";
8
8
  let mockBraveSecureKey: string | undefined;
9
9
  let mockPerplexitySecureKey: string | undefined;
10
10
  let mockTavilySecureKey: string | undefined;
11
+ let mockFirecrawlSecureKey: string | undefined;
11
12
  let mockManagedSearchProxyResult: any;
12
13
  let mockManagedSearchProxyCalls: Array<{
13
14
  provider: string;
@@ -40,6 +41,7 @@ mock.module("../../../security/secure-keys.js", () => ({
40
41
  if (provider === "brave") return mockBraveSecureKey;
41
42
  if (provider === "perplexity") return mockPerplexitySecureKey;
42
43
  if (provider === "tavily") return mockTavilySecureKey;
44
+ if (provider === "firecrawl") return mockFirecrawlSecureKey;
43
45
  return undefined;
44
46
  },
45
47
  }));
@@ -81,6 +83,7 @@ describe("web_search tool", () => {
81
83
  mockBraveSecureKey = undefined;
82
84
  mockPerplexitySecureKey = undefined;
83
85
  mockTavilySecureKey = undefined;
86
+ mockFirecrawlSecureKey = undefined;
84
87
  mockManagedSearchProxyCalls = [];
85
88
  mockManagedSearchProxyResult = {
86
89
  ok: true,
@@ -685,6 +688,129 @@ describe("web_search tool", () => {
685
688
  expect(callCount).toBe(4);
686
689
  });
687
690
 
691
+ // ---- Firecrawl provider -------------------------------------------------
692
+
693
+ test("executes Firecrawl search successfully", async () => {
694
+ mockWebSearchProvider = "firecrawl";
695
+ mockFirecrawlSecureKey = "fc-test-key";
696
+ globalThis.fetch = (async () => {
697
+ return new Response(
698
+ JSON.stringify({
699
+ success: true,
700
+ data: {
701
+ web: [
702
+ {
703
+ title: "Firecrawl Result 1",
704
+ url: "https://example.com/fc-1",
705
+ description: "First Firecrawl result",
706
+ },
707
+ {
708
+ title: "Firecrawl Result 2",
709
+ url: "https://example.com/fc-2",
710
+ description: "Second Firecrawl result",
711
+ },
712
+ ],
713
+ },
714
+ }),
715
+ { status: 200, headers: { "content-type": "application/json" } },
716
+ );
717
+ }) as any;
718
+
719
+ const result = await execute({ query: "what is TypeScript" });
720
+ expect(result.isError).toBe(false);
721
+ expect(result.content).toContain("Firecrawl Result 1");
722
+ expect(result.content).toContain("https://example.com/fc-1");
723
+ expect(result.content).toContain("First Firecrawl result");
724
+ });
725
+
726
+ test("Firecrawl sends correct request format", async () => {
727
+ mockWebSearchProvider = "firecrawl";
728
+ mockFirecrawlSecureKey = "fc-test-key";
729
+ let capturedUrl = "";
730
+ let capturedBody: any = null;
731
+ let capturedHeaders: any = null;
732
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
733
+ capturedUrl = url;
734
+ capturedBody = JSON.parse(init?.body as string);
735
+ capturedHeaders = new Headers(init?.headers);
736
+ return new Response(JSON.stringify({ success: true, data: { web: [] } }), {
737
+ status: 200,
738
+ headers: { "content-type": "application/json" },
739
+ });
740
+ }) as any;
741
+
742
+ await execute({ query: "test query", count: 50, freshness: "pm" });
743
+ expect(capturedUrl).toContain("api.firecrawl.dev/v2/search");
744
+ expect(capturedBody.query).toBe("test query");
745
+ expect(capturedBody.limit).toBe(20);
746
+ expect(capturedBody.sources).toEqual(["web"]);
747
+ expect(capturedBody.tbs).toBe("qdr:m");
748
+ expect(capturedHeaders.get("authorization")).toBe("Bearer fc-test-key");
749
+ expect(capturedHeaders.get("x-client-source")).toBe("vellum-assistant");
750
+ });
751
+
752
+ test("Firecrawl skips invalid freshness values", async () => {
753
+ mockWebSearchProvider = "firecrawl";
754
+ mockFirecrawlSecureKey = "fc-key";
755
+ let capturedBody: any = null;
756
+ globalThis.fetch = (async (_url: string, init?: RequestInit) => {
757
+ capturedBody = JSON.parse(init?.body as string);
758
+ return new Response(JSON.stringify({ success: true, data: { web: [] } }), {
759
+ status: 200,
760
+ headers: { "content-type": "application/json" },
761
+ });
762
+ }) as any;
763
+
764
+ await execute({ query: "test", freshness: "invalid" });
765
+ expect(capturedBody.tbs).toBeUndefined();
766
+ });
767
+
768
+ test("Firecrawl returns no results message when response is empty", async () => {
769
+ mockWebSearchProvider = "firecrawl";
770
+ mockFirecrawlSecureKey = "fc-key";
771
+ globalThis.fetch = (async () => {
772
+ return new Response(JSON.stringify({ success: true, data: { web: [] } }), {
773
+ status: 200,
774
+ headers: { "content-type": "application/json" },
775
+ });
776
+ }) as any;
777
+
778
+ const result = await execute({ query: "obscure query" });
779
+ expect(result.isError).toBe(false);
780
+ expect(result.content).toContain("No results found");
781
+ });
782
+
783
+ test.each([401, 403])("Firecrawl handles %d auth error", async (status) => {
784
+ mockWebSearchProvider = "firecrawl";
785
+ mockFirecrawlSecureKey = "bad-key";
786
+ globalThis.fetch = (async () => {
787
+ return new Response("Auth error", { status });
788
+ }) as any;
789
+
790
+ const result = await execute({ query: "test" });
791
+ expect(result.isError).toBe(true);
792
+ expect(result.content).toContain("Invalid or expired Firecrawl API key");
793
+ });
794
+
795
+ test("Firecrawl handles 429 rate limit after max retries", async () => {
796
+ mockWebSearchProvider = "firecrawl";
797
+ mockFirecrawlSecureKey = "fc-key";
798
+ let callCount = 0;
799
+ globalThis.fetch = (async () => {
800
+ callCount++;
801
+ return new Response("Too Many Requests", {
802
+ status: 429,
803
+ headers: { "retry-after": "0" },
804
+ });
805
+ }) as any;
806
+
807
+ const result = await execute({ query: "test" });
808
+ expect(result.isError).toBe(true);
809
+ // Post-retry rate limits surface the friendly recoverable copy (ATL-727).
810
+ expect(result.content).toBe(WEB_SEARCH_BACKEND_FAILURE_MESSAGE);
811
+ expect(callCount).toBe(4);
812
+ });
813
+
688
814
  // ---- Provider fallback --------------------------------------------------
689
815
 
690
816
  test("falls back from perplexity to brave when perplexity has no key", async () => {
@@ -740,6 +866,23 @@ describe("web_search tool", () => {
740
866
  expect(capturedUrl).toContain("tavily");
741
867
  });
742
868
 
869
+ test("falls back to firecrawl when all earlier providers have no key", async () => {
870
+ mockWebSearchProvider = "perplexity";
871
+ mockFirecrawlSecureKey = "fc-fallback-key";
872
+ let capturedUrl = "";
873
+ globalThis.fetch = (async (url: string) => {
874
+ capturedUrl = url;
875
+ return new Response(JSON.stringify({ success: true, data: { web: [] } }), {
876
+ status: 200,
877
+ headers: { "content-type": "application/json" },
878
+ });
879
+ }) as any;
880
+
881
+ const result = await execute({ query: "fallback test" });
882
+ expect(result.isError).toBe(false);
883
+ expect(capturedUrl).toContain("firecrawl");
884
+ });
885
+
743
886
  test("falls back from tavily to perplexity when tavily has no key", async () => {
744
887
  mockWebSearchProvider = "tavily";
745
888
  mockPerplexitySecureKey = "pplx-fallback-key";