@vellumai/assistant 0.9.0 → 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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -7,8 +7,8 @@
7
7
  * - `user-prompt-submit` — first-pass generation from the submitted prompt,
8
8
  * scheduled on a later macrotask so the main agent-loop LLM request is
9
9
  * issued first.
10
- * - `stop` — second-pass regeneration once the conversation reaches its third
11
- * user turn (turn count derived from the user prompts in history).
10
+ * - `stop` — fallback retry for replaceable titles and second-pass
11
+ * regeneration once the conversation reaches its third user turn.
12
12
  *
13
13
  * Both let the title service resolve the provider, persist the title, and
14
14
  * broadcast the resulting `conversation_title_updated` / `sync_changed`
@@ -31,13 +31,42 @@ const queueGenerateConversationTitleMock = mock(
31
31
  }): void => undefined,
32
32
  );
33
33
  const queueRegenerateConversationTitleMock = mock(
34
- (_params: { conversationId: string; provider?: unknown }): void => undefined,
34
+ (_params: {
35
+ conversationId: string;
36
+ provider?: unknown;
37
+ onlyIfReplaceable?: boolean;
38
+ }): void => undefined,
35
39
  );
36
40
  mock.module("../memory/conversation-title-service.js", () => ({
41
+ AUTO_TITLE_DETERMINISTIC: 2,
42
+ isReplaceableTitle: (title: string | null) =>
43
+ title == null ||
44
+ title === "" ||
45
+ title === "Generating title..." ||
46
+ title === "New Conversation" ||
47
+ title === "Untitled" ||
48
+ title === "Untitled Conversation" ||
49
+ title.startsWith("Runtime: "),
37
50
  queueGenerateConversationTitle: queueGenerateConversationTitleMock,
38
51
  queueRegenerateConversationTitle: queueRegenerateConversationTitleMock,
39
52
  }));
40
53
 
54
+ const mockGetConversation = mock(
55
+ (_conversationId: string) =>
56
+ ({
57
+ title: "Existing Title",
58
+ isAutoTitle: 1,
59
+ conversationType: "standard",
60
+ }) as {
61
+ title: string;
62
+ isAutoTitle: number;
63
+ conversationType: string;
64
+ },
65
+ );
66
+ mock.module("../memory/conversation-crud.js", () => ({
67
+ getConversation: mockGetConversation,
68
+ }));
69
+
41
70
  // The `stop` hook reads `conversations.skipAutoRetitling`; stub the loader so
42
71
  // the opt-out is controllable per test.
43
72
  let skipAutoRetitling = false;
@@ -192,6 +221,19 @@ describe("title-generate stop hook", () => {
192
221
  resetPluginRegistryForTests();
193
222
  queueRegenerateConversationTitleMock.mockReset();
194
223
  queueRegenerateConversationTitleMock.mockImplementation(() => undefined);
224
+ mockGetConversation.mockReset();
225
+ mockGetConversation.mockImplementation(
226
+ (_conversationId: string) =>
227
+ ({
228
+ title: "Existing Title",
229
+ isAutoTitle: 1,
230
+ conversationType: "standard",
231
+ }) as {
232
+ title: string;
233
+ isAutoTitle: number;
234
+ conversationType: string;
235
+ },
236
+ );
195
237
  skipAutoRetitling = false;
196
238
  });
197
239
 
@@ -210,6 +252,61 @@ describe("title-generate stop hook", () => {
210
252
  expect(call?.conversationId).toBe("conv-1");
211
253
  expect(call).not.toHaveProperty("provider");
212
254
  expect(call).not.toHaveProperty("signal");
255
+ expect(call).not.toHaveProperty("onlyIfReplaceable");
256
+ });
257
+
258
+ test("retries a replaceable fallback title after a successful turn", async () => {
259
+ mockGetConversation.mockReturnValueOnce({
260
+ title: "Untitled Conversation",
261
+ isAutoTitle: 2,
262
+ conversationType: "standard",
263
+ });
264
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
265
+
266
+ await stop(ctx);
267
+ await flushMacrotasks();
268
+
269
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
270
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
271
+ conversationId: "conv-1",
272
+ onlyIfReplaceable: true,
273
+ });
274
+ });
275
+
276
+ test("preserves the third-turn retitle when the title is still replaceable", async () => {
277
+ mockGetConversation.mockReturnValueOnce({
278
+ title: "Untitled Conversation",
279
+ isAutoTitle: 2,
280
+ conversationType: "standard",
281
+ });
282
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
283
+
284
+ await stop(ctx);
285
+ await flushMacrotasks();
286
+
287
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
288
+ const call = queueRegenerateConversationTitleMock.mock.calls[0]?.[0];
289
+ expect(call?.conversationId).toBe("conv-1");
290
+ expect(call).not.toHaveProperty("onlyIfReplaceable");
291
+ });
292
+
293
+ test("fallback title retry is not blocked by the retitling opt-out", async () => {
294
+ skipAutoRetitling = true;
295
+ mockGetConversation.mockReturnValueOnce({
296
+ title: "Generating title...",
297
+ isAutoTitle: 1,
298
+ conversationType: "standard",
299
+ });
300
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
301
+
302
+ await stop(ctx);
303
+ await flushMacrotasks();
304
+
305
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
306
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
307
+ conversationId: "conv-1",
308
+ onlyIfReplaceable: true,
309
+ });
213
310
  });
214
311
 
215
312
  test("defers the regeneration so the completed turn is persisted first", async () => {
@@ -128,7 +128,6 @@ function makeSystemPrompt(size: "small" | "production" = "small"): string {
128
128
  "- `file_write` — Creating or overwriting files",
129
129
  "- `file_edit` — Modifying existing files",
130
130
  "- `file_delete` — Deleting files",
131
- "- `credential_store set` — Storing credentials",
132
131
  "",
133
132
  "### Medium-Risk Tools (require approval on first use per session)",
134
133
  "- `web_fetch` — Fetching external URLs",
@@ -349,7 +348,7 @@ function makeToolDefinitions(): Array<{
349
348
  input_schema: object;
350
349
  }> = [];
351
350
 
352
- // Core tools (11)
351
+ // Core tools (10)
353
352
  tools.push(
354
353
  {
355
354
  name: "bash",
@@ -566,33 +565,6 @@ function makeToolDefinitions(): Array<{
566
565
  required: ["query"],
567
566
  },
568
567
  },
569
- {
570
- name: "credential_store",
571
- description:
572
- "Securely store or retrieve credentials for external services. Credentials are encrypted at rest.",
573
- input_schema: {
574
- type: "object",
575
- properties: {
576
- action: {
577
- type: "string",
578
- enum: ["get", "set", "delete", "list"],
579
- },
580
- service: {
581
- type: "string",
582
- description: "The service name (e.g., 'github', 'slack')",
583
- },
584
- key: {
585
- type: "string",
586
- description: "Credential key within the service",
587
- },
588
- value: {
589
- type: "string",
590
- description: "Credential value (required for 'set')",
591
- },
592
- },
593
- required: ["action", "service"],
594
- },
595
- },
596
568
  );
597
569
 
598
570
  // Computer-use proxy tools (11)
@@ -0,0 +1,519 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync, readdirSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ afterAll,
7
+ afterEach,
8
+ beforeAll,
9
+ beforeEach,
10
+ describe,
11
+ expect,
12
+ mock,
13
+ test,
14
+ } from "bun:test";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Mock logger
18
+ // ---------------------------------------------------------------------------
19
+
20
+ mock.module("../util/logger.js", () => ({
21
+ getLogger: () =>
22
+ new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
+ }));
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Use encrypted backend with a temp store path
29
+ // ---------------------------------------------------------------------------
30
+
31
+ import { _resetBackend, setSecureKeyAsync } from "../security/secure-keys.js";
32
+ import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
33
+
34
+ const TEST_DIR = join(
35
+ tmpdir(),
36
+ `vellum-token-manager-test-${randomBytes(4).toString("hex")}`,
37
+ );
38
+ const STORE_PATH = join(TEST_DIR, "keys.enc");
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Mock OAuth2 token refresh so dedup can be observed without network I/O
42
+ // ---------------------------------------------------------------------------
43
+
44
+ let mockRefreshOAuth2Token: ReturnType<
45
+ typeof mock<
46
+ (
47
+ tokenExchangeUrl: string,
48
+ clientId: string,
49
+ refreshToken: string,
50
+ clientSecret?: string,
51
+ tokenEndpointAuthMethod?: string,
52
+ ) => Promise<{ accessToken: string; expiresIn: number }>
53
+ >
54
+ >;
55
+
56
+ mock.module("../security/oauth2.js", () => {
57
+ mockRefreshOAuth2Token = mock(() =>
58
+ Promise.resolve({
59
+ accessToken: "refreshed-access-token",
60
+ expiresIn: 3600,
61
+ }),
62
+ );
63
+ return {
64
+ refreshOAuth2Token: mockRefreshOAuth2Token,
65
+ };
66
+ });
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Mock oauth-store — token-manager reads refresh config from SQLite
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /** Mutable per-test map of provider connections for getConnectionByProvider */
73
+ const mockConnections = new Map<
74
+ string,
75
+ {
76
+ id: string;
77
+ provider: string;
78
+ oauthAppId: string;
79
+ expiresAt: number | null;
80
+ }
81
+ >();
82
+ const mockApps = new Map<
83
+ string,
84
+ {
85
+ id: string;
86
+ provider: string;
87
+ clientId: string;
88
+ clientSecretCredentialPath: string;
89
+ }
90
+ >();
91
+ const mockProviders = new Map<
92
+ string,
93
+ {
94
+ key: string;
95
+ tokenExchangeUrl: string;
96
+ refreshUrl?: string | null;
97
+ tokenEndpointAuthMethod?: string;
98
+ }
99
+ >();
100
+
101
+ mock.module("../oauth/oauth-store.js", () => ({
102
+ getConnectionByProvider: (service: string) => mockConnections.get(service),
103
+ getConnection: (id: string) => {
104
+ for (const conn of mockConnections.values()) {
105
+ if (conn.id === id) return conn;
106
+ }
107
+ return undefined;
108
+ },
109
+ getApp: (id: string) => mockApps.get(id),
110
+ getProvider: (key: string) => mockProviders.get(key),
111
+ updateConnection: () => {},
112
+ }));
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Import the modules under test
116
+ // ---------------------------------------------------------------------------
117
+
118
+ import {
119
+ _resetInflightRefreshes,
120
+ _resetRefreshBreakers,
121
+ withValidToken,
122
+ } from "../security/token-manager.js";
123
+ import { _setMetadataPath } from "../tools/credentials/metadata-store.js";
124
+
125
+ describe("withValidToken refresh deduplication", () => {
126
+ beforeAll(() => {
127
+ mkdirSync(TEST_DIR, { recursive: true });
128
+ });
129
+
130
+ beforeEach(() => {
131
+ _resetBackend();
132
+ for (const entry of readdirSync(TEST_DIR)) {
133
+ rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
134
+ }
135
+ setStorePathForTesting(STORE_PATH);
136
+ _setMetadataPath(join(TEST_DIR, "metadata.json"));
137
+ _resetRefreshBreakers();
138
+ _resetInflightRefreshes();
139
+ mockRefreshOAuth2Token.mockClear();
140
+ // Clear mock oauth-store maps
141
+ mockConnections.clear();
142
+ mockApps.clear();
143
+ mockProviders.clear();
144
+ });
145
+
146
+ afterEach(() => {
147
+ _setMetadataPath(null);
148
+ setStorePathForTesting(null);
149
+ _resetBackend();
150
+ _resetRefreshBreakers();
151
+ _resetInflightRefreshes();
152
+ mockConnections.clear();
153
+ mockApps.clear();
154
+ mockProviders.clear();
155
+ });
156
+
157
+ afterAll(() => {
158
+ rmSync(TEST_DIR, { recursive: true, force: true });
159
+ });
160
+
161
+ /**
162
+ * Helper: set up a service with an access token, refresh token, and
163
+ * mock DB data so that token refresh can proceed through doRefresh().
164
+ *
165
+ * OAuth-specific fields (tokenExchangeUrl, clientId, expiresAt) are stored
166
+ * in the SQLite oauth-store. The mock maps simulate the DB layer.
167
+ */
168
+ async function setupService(
169
+ service: string,
170
+ opts?: { expired?: boolean; accessToken?: string },
171
+ ) {
172
+ const accessToken = opts?.accessToken ?? "old-access-token";
173
+
174
+ // Seed mock oauth-store maps so token-manager can resolve refresh config
175
+ const appId = `app-${service}`;
176
+ const connId = `conn-${service}`;
177
+
178
+ // Store access token under the oauth_connection key path that
179
+ // withValidToken reads (not the legacy credentialKey path).
180
+ await setSecureKeyAsync(
181
+ `oauth_connection/${connId}/access_token`,
182
+ accessToken,
183
+ );
184
+ mockProviders.set(service, {
185
+ key: service,
186
+ tokenExchangeUrl: "https://oauth.example.com/token",
187
+ refreshUrl: null,
188
+ });
189
+ mockApps.set(appId, {
190
+ id: appId,
191
+ provider: service,
192
+ clientId: "test-client-id",
193
+ clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
194
+ });
195
+ mockConnections.set(service, {
196
+ id: connId,
197
+ provider: service,
198
+ oauthAppId: appId,
199
+ expiresAt: opts?.expired
200
+ ? Date.now() - 60_000 // expired 1 minute ago
201
+ : Date.now() + 3600_000, // expires in 1 hour
202
+ });
203
+ // Store refresh token and client_secret in secure keys (token-manager reads them)
204
+ await setSecureKeyAsync(
205
+ `oauth_connection/${connId}/refresh_token`,
206
+ "valid-refresh-token",
207
+ );
208
+ await setSecureKeyAsync(
209
+ `oauth_app/${appId}/client_secret`,
210
+ "test-client-secret",
211
+ );
212
+ }
213
+
214
+ test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
215
+ await setupService("google");
216
+
217
+ let resolveRefresh!: (value: {
218
+ accessToken: string;
219
+ expiresIn: number;
220
+ }) => void;
221
+ const refreshPromise = new Promise<{
222
+ accessToken: string;
223
+ expiresIn: number;
224
+ }>((resolve) => {
225
+ resolveRefresh = resolve;
226
+ });
227
+
228
+ mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
229
+
230
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
231
+
232
+ const callback = async (token: string) => {
233
+ if (token === "old-access-token") throw err401;
234
+ return `result-with-${token}`;
235
+ };
236
+
237
+ // Launch 3 concurrent withValidToken calls — all will get a non-expired
238
+ // token first, call the callback, get a 401, and then try to refresh.
239
+ const p1 = withValidToken("google", callback);
240
+ const p2 = withValidToken("google", callback);
241
+ const p3 = withValidToken("google", callback);
242
+
243
+ // Let the event loop tick so all 3 calls enter the 401 retry path
244
+ await new Promise((r) => setTimeout(r, 10));
245
+
246
+ // Resolve the single refresh attempt
247
+ resolveRefresh({ accessToken: "new-token-123", expiresIn: 3600 });
248
+
249
+ const results = await Promise.all([p1, p2, p3]);
250
+
251
+ // All 3 should succeed with the refreshed token
252
+ expect(results).toEqual([
253
+ "result-with-new-token-123",
254
+ "result-with-new-token-123",
255
+ "result-with-new-token-123",
256
+ ]);
257
+
258
+ // refreshOAuth2Token should have been called exactly once
259
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
260
+ });
261
+
262
+ test("concurrent refreshes for different services proceed independently", async () => {
263
+ await setupService("google");
264
+ await setupService("slack");
265
+
266
+ let resolveGmail!: (value: {
267
+ accessToken: string;
268
+ expiresIn: number;
269
+ }) => void;
270
+ let resolveSlack!: (value: {
271
+ accessToken: string;
272
+ expiresIn: number;
273
+ }) => void;
274
+
275
+ const gmailPromise = new Promise<{
276
+ accessToken: string;
277
+ expiresIn: number;
278
+ }>((resolve) => {
279
+ resolveGmail = resolve;
280
+ });
281
+ const slackPromise = new Promise<{
282
+ accessToken: string;
283
+ expiresIn: number;
284
+ }>((resolve) => {
285
+ resolveSlack = resolve;
286
+ });
287
+
288
+ let refreshCallCount = 0;
289
+ mockRefreshOAuth2Token.mockImplementation(() => {
290
+ refreshCallCount++;
291
+ // Both services use the same tokenExchangeUrl in this test, so we track by
292
+ // call order to return the correct deferred promise.
293
+ if (refreshCallCount === 1) return gmailPromise;
294
+ return slackPromise;
295
+ });
296
+
297
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
298
+
299
+ const gmailCallback = async (token: string) => {
300
+ if (token === "old-access-token") throw err401;
301
+ return `gmail-${token}`;
302
+ };
303
+ const slackCallback = async (token: string) => {
304
+ if (token === "old-access-token") throw err401;
305
+ return `slack-${token}`;
306
+ };
307
+
308
+ const p1 = withValidToken("google", gmailCallback);
309
+ const p2 = withValidToken("slack", slackCallback);
310
+
311
+ await new Promise((r) => setTimeout(r, 10));
312
+
313
+ // Resolve both independently
314
+ resolveGmail({ accessToken: "gmail-new-token", expiresIn: 3600 });
315
+ resolveSlack({ accessToken: "slack-new-token", expiresIn: 3600 });
316
+
317
+ const [r1, r2] = await Promise.all([p1, p2]);
318
+
319
+ expect(r1).toBe("gmail-gmail-new-token");
320
+ expect(r2).toBe("slack-slack-new-token");
321
+
322
+ // Both services should have triggered their own refresh
323
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(2);
324
+ });
325
+
326
+ test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
327
+ await setupService("google");
328
+
329
+ let refreshCount = 0;
330
+ mockRefreshOAuth2Token.mockImplementation(() => {
331
+ refreshCount++;
332
+ return Promise.resolve({
333
+ accessToken: `token-${refreshCount}`,
334
+ expiresIn: 3600,
335
+ });
336
+ });
337
+
338
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
339
+
340
+ // First call triggers a refresh (old token → 401 → refresh → token-1)
341
+ const r1 = await withValidToken("google", async (token: string) => {
342
+ if (token !== "token-1") throw err401;
343
+ return token;
344
+ });
345
+ expect(r1).toBe("token-1");
346
+ expect(refreshCount).toBe(1);
347
+
348
+ // Second call also triggers a 401 to verify dedup state was cleaned up
349
+ // and a new refresh is allowed (not deduplicated with the first).
350
+ const r2 = await withValidToken("google", async (token: string) => {
351
+ if (token !== "token-2") throw err401;
352
+ return token;
353
+ });
354
+ expect(r2).toBe("token-2");
355
+ // Second refresh should have happened (not deduplicated with the first,
356
+ // since the first already completed)
357
+ expect(refreshCount).toBe(2);
358
+ });
359
+
360
+ test("deduplication propagates refresh errors to all waiting callers", async () => {
361
+ await setupService("google");
362
+
363
+ mockRefreshOAuth2Token.mockImplementation(() =>
364
+ Promise.reject(
365
+ Object.assign(
366
+ new Error("OAuth2 token refresh failed (HTTP 401: invalid_grant)"),
367
+ ),
368
+ ),
369
+ );
370
+
371
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
372
+
373
+ const callback = async (token: string) => {
374
+ if (token === "old-access-token") throw err401;
375
+ return "should-not-reach";
376
+ };
377
+
378
+ // Launch 2 concurrent calls — both should fail with the same error
379
+ const p1 = withValidToken("google", callback);
380
+ const p2 = withValidToken("google", callback);
381
+
382
+ const results = await Promise.allSettled([p1, p2]);
383
+
384
+ expect(results[0].status).toBe("rejected");
385
+ expect(results[1].status).toBe("rejected");
386
+
387
+ // Only one actual refresh attempt
388
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
389
+ });
390
+
391
+ // -----------------------------------------------------------------------
392
+ // refreshUrl resolution — provider.refreshUrl with fallback to tokenExchangeUrl
393
+ // -----------------------------------------------------------------------
394
+ describe("refreshUrl resolution", () => {
395
+ test("uses provider.refreshUrl when set", async () => {
396
+ await setupService("google");
397
+ mockProviders.get("google")!.refreshUrl =
398
+ "https://refresh.example.com/token";
399
+
400
+ mockRefreshOAuth2Token.mockImplementation(() =>
401
+ Promise.resolve({
402
+ accessToken: "new-token-from-refresh-url",
403
+ expiresIn: 3600,
404
+ }),
405
+ );
406
+
407
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
408
+
409
+ const callback = async (token: string) => {
410
+ if (token === "old-access-token") throw err401;
411
+ return `result-with-${token}`;
412
+ };
413
+
414
+ const result = await withValidToken("google", callback);
415
+
416
+ expect(result).toBe("result-with-new-token-from-refresh-url");
417
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
418
+ // Assert the refresh endpoint passed in is provider.refreshUrl, not
419
+ // the tokenExchangeUrl fallback.
420
+ expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
421
+ "https://refresh.example.com/token",
422
+ );
423
+ });
424
+
425
+ test("falls back to provider.tokenExchangeUrl when refreshUrl is null", async () => {
426
+ // setupService sets refreshUrl: null by default — this exercises the
427
+ // fallback path explicitly.
428
+ await setupService("google");
429
+ expect(mockProviders.get("google")!.refreshUrl).toBeNull();
430
+
431
+ mockRefreshOAuth2Token.mockImplementation(() =>
432
+ Promise.resolve({
433
+ accessToken: "new-token-from-token-exchange-url",
434
+ expiresIn: 3600,
435
+ }),
436
+ );
437
+
438
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
439
+
440
+ const callback = async (token: string) => {
441
+ if (token === "old-access-token") throw err401;
442
+ return `result-with-${token}`;
443
+ };
444
+
445
+ const result = await withValidToken("google", callback);
446
+
447
+ expect(result).toBe("result-with-new-token-from-token-exchange-url");
448
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
449
+ // Assert the refresh endpoint falls back to tokenExchangeUrl.
450
+ expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
451
+ "https://oauth.example.com/token",
452
+ );
453
+ });
454
+
455
+ test("falls back to provider.tokenExchangeUrl when refreshUrl is undefined", async () => {
456
+ await setupService("google");
457
+ // Delete the refreshUrl field entirely so the property is `undefined`
458
+ // rather than `null`. Both representations of "not set" must produce
459
+ // the fallback behavior.
460
+ delete mockProviders.get("google")!.refreshUrl;
461
+ expect(mockProviders.get("google")!.refreshUrl).toBeUndefined();
462
+
463
+ mockRefreshOAuth2Token.mockImplementation(() =>
464
+ Promise.resolve({
465
+ accessToken: "new-token-from-token-exchange-url",
466
+ expiresIn: 3600,
467
+ }),
468
+ );
469
+
470
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
471
+
472
+ const callback = async (token: string) => {
473
+ if (token === "old-access-token") throw err401;
474
+ return `result-with-${token}`;
475
+ };
476
+
477
+ const result = await withValidToken("google", callback);
478
+
479
+ expect(result).toBe("result-with-new-token-from-token-exchange-url");
480
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
481
+ // Assert the refresh endpoint falls back to tokenExchangeUrl.
482
+ expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
483
+ "https://oauth.example.com/token",
484
+ );
485
+ });
486
+
487
+ test("falls back to provider.tokenExchangeUrl when refreshUrl is empty string", async () => {
488
+ // Platform's Python `oauth_app.refresh_url or oauth_app.token_exchange_url`
489
+ // treats an empty string as unset. We use `||` (not `??`) so empty
490
+ // strings follow the same fallback path and never resolve to an empty
491
+ // endpoint.
492
+ await setupService("google");
493
+ mockProviders.get("google")!.refreshUrl = "";
494
+
495
+ mockRefreshOAuth2Token.mockImplementation(() =>
496
+ Promise.resolve({
497
+ accessToken: "new-token-from-token-exchange-url",
498
+ expiresIn: 3600,
499
+ }),
500
+ );
501
+
502
+ const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
503
+
504
+ const callback = async (token: string) => {
505
+ if (token === "old-access-token") throw err401;
506
+ return `result-with-${token}`;
507
+ };
508
+
509
+ const result = await withValidToken("google", callback);
510
+
511
+ expect(result).toBe("result-with-new-token-from-token-exchange-url");
512
+ expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
513
+ // Assert the refresh endpoint falls back to tokenExchangeUrl — NOT "".
514
+ expect(mockRefreshOAuth2Token.mock.calls[0]?.[0]).toBe(
515
+ "https://oauth.example.com/token",
516
+ );
517
+ });
518
+ });
519
+ });