@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Tests for plugin-contributed skills (PR 33).
3
+ *
4
+ * Covers:
5
+ * - A plugin declaring `skills: [...]` has its entries registered after
6
+ * bootstrap's `init()` succeeds.
7
+ * - The registered skill is discoverable via `loadSkillCatalog` and
8
+ * resolvable via `loadSkillBySelector` (the exact entry points the model's
9
+ * `skill_load` / `skill_execute` flow use).
10
+ * - Shutdown (runShutdownHooks) unregisters the plugin's skills so repeated
11
+ * bootstraps don't leak catalog entries.
12
+ * - Ref-counted register/unregister semantics match the tool registry's
13
+ * per-skill-id semantics (PR 13 precedent).
14
+ *
15
+ * Strategy mirrors `plugin-bootstrap.test.ts`: stub the credential store so
16
+ * bootstrap doesn't hit real backends, and `resetPluginRegistryForTests` /
17
+ * `resetPluginSkillContributionsForTests` between cases for isolation.
18
+ */
19
+
20
+ import { rm } from "node:fs/promises";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
24
+
25
+ // Stub credential store before importing bootstrap so the module captures
26
+ // the fake binding.
27
+ const getSecureKeyAsyncMock = mock(
28
+ async (_account: string): Promise<string | undefined> => undefined,
29
+ );
30
+ mock.module("../security/secure-keys.js", () => ({
31
+ getSecureKeyAsync: getSecureKeyAsyncMock,
32
+ }));
33
+
34
+ import type { AssistantConfig } from "../config/schema.js";
35
+ import { loadSkillBySelector, loadSkillCatalog } from "../config/skills.js";
36
+ import {
37
+ bootstrapPlugins,
38
+ type DaemonContext,
39
+ } from "../daemon/external-plugins-bootstrap.js";
40
+ import { runShutdownHooks } from "../daemon/shutdown-registry.js";
41
+ import {
42
+ getPluginContributedSkillDefinition,
43
+ getPluginContributedSkillSummaries,
44
+ getPluginSkillRefCount,
45
+ registerPluginSkills,
46
+ resetPluginSkillContributionsForTests,
47
+ unregisterPluginSkills,
48
+ } from "../plugins/plugin-skill-contributions.js";
49
+ import {
50
+ registerPlugin,
51
+ resetPluginRegistryForTests,
52
+ } from "../plugins/registry.js";
53
+ import {
54
+ type Plugin,
55
+ PluginExecutionError,
56
+ type PluginSkillRegistration,
57
+ } from "../plugins/types.js";
58
+
59
+ // Per-process temp tree so bootstrap's plugin-storage directory creation
60
+ // doesn't touch the developer's real ~/.vellum.
61
+ const TEST_INSTANCE_DIR = join(
62
+ tmpdir(),
63
+ `vellum-plugin-skill-test-${process.pid}`,
64
+ );
65
+ process.env.BASE_DATA_DIR = TEST_INSTANCE_DIR;
66
+
67
+ const fakeConfig = {} as unknown as AssistantConfig;
68
+ const fakeCtx: DaemonContext = {
69
+ config: fakeConfig,
70
+ assistantVersion: "9.9.9-test",
71
+ };
72
+
73
+ /** Build a plugin that contributes one or more skills. */
74
+ function buildSkillPlugin(
75
+ name: string,
76
+ skills: PluginSkillRegistration[],
77
+ extras: Partial<Omit<Plugin, "manifest" | "skills">> = {},
78
+ ): Plugin {
79
+ return {
80
+ manifest: {
81
+ name,
82
+ version: "0.0.1",
83
+ requires: { pluginRuntime: "v1" },
84
+ },
85
+ skills,
86
+ ...extras,
87
+ };
88
+ }
89
+
90
+ describe("plugin skill contributions", () => {
91
+ beforeEach(async () => {
92
+ resetPluginRegistryForTests();
93
+ resetPluginSkillContributionsForTests();
94
+ getSecureKeyAsyncMock.mockReset();
95
+ getSecureKeyAsyncMock.mockImplementation(async () => undefined);
96
+ await rm(TEST_INSTANCE_DIR, { recursive: true, force: true });
97
+ });
98
+
99
+ test("plugin skills are registered after bootstrap and exposed by the catalog", async () => {
100
+ const skill: PluginSkillRegistration = {
101
+ id: "plugin-demo-skill",
102
+ name: "plugin-demo",
103
+ displayName: "Plugin Demo",
104
+ description: "A skill contributed by a plugin",
105
+ body: "# Plugin Demo\n\nThis is the plugin-provided body.",
106
+ activationHints: ["demo", "plugin"],
107
+ };
108
+
109
+ registerPlugin(buildSkillPlugin("demo-plugin", [skill]));
110
+ await bootstrapPlugins(fakeCtx);
111
+
112
+ // Ref count bumped to exactly 1 so we can tell register and unregister
113
+ // are balanced downstream.
114
+ expect(getPluginSkillRefCount("demo-plugin")).toBe(1);
115
+
116
+ // In-memory registry query surfaces the summary form.
117
+ const summaries = getPluginContributedSkillSummaries();
118
+ const registered = summaries.find((s) => s.id === "plugin-demo-skill");
119
+ expect(registered).toBeDefined();
120
+ expect(registered?.source).toBe("plugin");
121
+ expect(registered?.name).toBe("plugin-demo");
122
+ expect(registered?.displayName).toBe("Plugin Demo");
123
+ expect(registered?.activationHints).toEqual(["demo", "plugin"]);
124
+ // Summary must not carry the body — bodies are definition-only.
125
+ expect((registered as unknown as { body?: unknown }).body).toBeUndefined();
126
+
127
+ // The full definition is retrievable by id.
128
+ const def = getPluginContributedSkillDefinition("plugin-demo-skill");
129
+ expect(def).toBeDefined();
130
+ expect(def?.body).toContain("This is the plugin-provided body");
131
+ });
132
+
133
+ test("catalog and loadSkillBySelector discover plugin-contributed skills (skill_load pathway)", async () => {
134
+ const skill: PluginSkillRegistration = {
135
+ id: "catalog-visible-skill",
136
+ name: "catalog-visible",
137
+ description: "A plugin skill expected to surface in loadSkillCatalog",
138
+ body: "# Catalog-Visible Skill\n\nBody content for skill_load.",
139
+ };
140
+
141
+ registerPlugin(buildSkillPlugin("catalog-plugin", [skill]));
142
+ await bootstrapPlugins(fakeCtx);
143
+
144
+ // loadSkillCatalog is the exact entry point `skill_load` consults via
145
+ // `loadSkillBySelector` -> `resolveSkillSelector`.
146
+ const catalog = loadSkillCatalog();
147
+ const found = catalog.find((s) => s.id === "catalog-visible-skill");
148
+ expect(found).toBeDefined();
149
+ expect(found?.source).toBe("plugin");
150
+ expect(found?.description).toBe(skill.description);
151
+
152
+ // loadSkillBySelector is what SkillLoadTool.execute calls — it must
153
+ // return a fully-populated SkillDefinition, including the body.
154
+ const lookup = loadSkillBySelector("catalog-visible-skill");
155
+ expect(lookup.error).toBeUndefined();
156
+ expect(lookup.skill).toBeDefined();
157
+ expect(lookup.skill?.id).toBe("catalog-visible-skill");
158
+ expect(lookup.skill?.body).toContain("Body content for skill_load");
159
+
160
+ // And name-based resolution (what users type when the model picks a
161
+ // skill by name) works the same way.
162
+ const byName = loadSkillBySelector("catalog-visible");
163
+ expect(byName.skill?.id).toBe("catalog-visible-skill");
164
+ });
165
+
166
+ test("shutdown unregisters plugin skills so the catalog no longer lists them", async () => {
167
+ const skill: PluginSkillRegistration = {
168
+ id: "ephemeral-skill",
169
+ name: "ephemeral",
170
+ description: "Disappears after shutdown",
171
+ body: "Only visible while the plugin is alive.",
172
+ };
173
+
174
+ registerPlugin(buildSkillPlugin("ephemeral-plugin", [skill]));
175
+ await bootstrapPlugins(fakeCtx);
176
+
177
+ // Sanity: present before shutdown.
178
+ expect(loadSkillCatalog().some((s) => s.id === "ephemeral-skill")).toBe(
179
+ true,
180
+ );
181
+ expect(getPluginSkillRefCount("ephemeral-plugin")).toBe(1);
182
+
183
+ await runShutdownHooks("test-shutdown");
184
+
185
+ // After shutdown, the plugin's skill is gone from both the in-memory
186
+ // registry and any catalog view that consults it.
187
+ expect(getPluginSkillRefCount("ephemeral-plugin")).toBe(0);
188
+ expect(
189
+ getPluginContributedSkillDefinition("ephemeral-skill"),
190
+ ).toBeUndefined();
191
+
192
+ const catalogAfter = loadSkillCatalog();
193
+ expect(catalogAfter.some((s) => s.id === "ephemeral-skill")).toBe(false);
194
+
195
+ // And the selector lookup the model would use fails closed.
196
+ const lookup = loadSkillBySelector("ephemeral-skill");
197
+ expect(lookup.skill).toBeUndefined();
198
+ });
199
+
200
+ test("bootstrap is a no-op for plugins without a skills list", async () => {
201
+ // A plugin with no `skills` field must not bump ref counts or
202
+ // populate the catalog at all.
203
+ registerPlugin({
204
+ manifest: {
205
+ name: "no-skills-plugin",
206
+ version: "0.0.1",
207
+ requires: { pluginRuntime: "v1" },
208
+ },
209
+ });
210
+
211
+ await bootstrapPlugins(fakeCtx);
212
+
213
+ expect(getPluginSkillRefCount("no-skills-plugin")).toBe(0);
214
+ expect(getPluginContributedSkillSummaries()).toEqual([]);
215
+ });
216
+
217
+ test("duplicate skill id across plugins fails bootstrap with the plugin named", async () => {
218
+ const shared: PluginSkillRegistration = {
219
+ id: "contested-id",
220
+ name: "contested",
221
+ description: "First",
222
+ body: "from first plugin",
223
+ };
224
+ const duplicate: PluginSkillRegistration = {
225
+ id: "contested-id",
226
+ name: "contested",
227
+ description: "Second",
228
+ body: "from second plugin",
229
+ };
230
+
231
+ registerPlugin(buildSkillPlugin("first-plugin", [shared]));
232
+ registerPlugin(buildSkillPlugin("second-plugin", [duplicate]));
233
+
234
+ let caught: unknown;
235
+ try {
236
+ await bootstrapPlugins(fakeCtx);
237
+ } catch (err) {
238
+ caught = err;
239
+ }
240
+ expect(caught).toBeInstanceOf(PluginExecutionError);
241
+ // The error must identify which plugin tripped the collision so
242
+ // operators can deploy a fix.
243
+ const msg = (caught as PluginExecutionError).message;
244
+ expect(msg).toContain("second-plugin");
245
+ expect(msg).toContain("contested-id");
246
+ });
247
+
248
+ test("intra-batch duplicate skill id in one plugin is rejected at registration", () => {
249
+ // Directly exercise the registry (no bootstrap) — a single plugin
250
+ // declaring the same id twice is a configuration bug and must fail
251
+ // loudly rather than silently overwrite.
252
+ expect(() =>
253
+ registerPluginSkills("dup-plugin", [
254
+ {
255
+ id: "dup-id",
256
+ name: "one",
257
+ description: "first",
258
+ body: "A",
259
+ },
260
+ {
261
+ id: "dup-id",
262
+ name: "two",
263
+ description: "second",
264
+ body: "B",
265
+ },
266
+ ]),
267
+ ).toThrow(PluginExecutionError);
268
+ expect(() =>
269
+ registerPluginSkills("dup-plugin", [
270
+ { id: "x", name: "one", description: "a", body: "A" },
271
+ { id: "x", name: "two", description: "b", body: "B" },
272
+ ]),
273
+ ).toThrow(/declared skill "x" more than once/);
274
+ });
275
+
276
+ test("ref-counted unregister: second unregister call after repeated registers drops skills", () => {
277
+ const registrations: PluginSkillRegistration[] = [
278
+ {
279
+ id: "refcount-skill",
280
+ name: "refcount",
281
+ description: "Ref-count demo",
282
+ body: "stays until the counter hits zero",
283
+ },
284
+ ];
285
+
286
+ registerPluginSkills("refcount-plugin", registrations);
287
+ // Second call is the "same plugin registered again" pattern (hot
288
+ // reload). It must bump the counter without re-inserting.
289
+ registerPluginSkills("refcount-plugin", registrations);
290
+ expect(getPluginSkillRefCount("refcount-plugin")).toBe(2);
291
+ expect(getPluginContributedSkillDefinition("refcount-skill")).toBeDefined();
292
+
293
+ // One unregister decrements only — skill must still be registered.
294
+ unregisterPluginSkills("refcount-plugin");
295
+ expect(getPluginSkillRefCount("refcount-plugin")).toBe(1);
296
+ expect(getPluginContributedSkillDefinition("refcount-skill")).toBeDefined();
297
+
298
+ // Second unregister drops the entry for real.
299
+ unregisterPluginSkills("refcount-plugin");
300
+ expect(getPluginSkillRefCount("refcount-plugin")).toBe(0);
301
+ expect(
302
+ getPluginContributedSkillDefinition("refcount-skill"),
303
+ ).toBeUndefined();
304
+
305
+ // Third unregister (over-decrement) is a no-op, not a throw — mirrors
306
+ // the tool-registry behavior so shutdown races don't crash the
307
+ // daemon.
308
+ expect(() => unregisterPluginSkills("refcount-plugin")).not.toThrow();
309
+ });
310
+
311
+ test("managed (filesystem) skill with the same id overrides a plugin-contributed skill", async () => {
312
+ // Plugin skills sit below managed/workspace in the catalog precedence
313
+ // chain so a user can shadow a plugin-provided skill by dropping a
314
+ // SKILL.md with the same id under ~/.vellum/workspace/skills. The test
315
+ // harness configures VELLUM_WORKSPACE_DIR via the bun test preload, so
316
+ // we can synthesize a filesystem skill for the same id and assert it
317
+ // wins.
318
+ const { mkdirSync, writeFileSync, rmSync, existsSync } =
319
+ await import("node:fs");
320
+
321
+ const workspaceDir = process.env.VELLUM_WORKSPACE_DIR;
322
+ // Skip the filesystem override check when the harness did not provide a
323
+ // workspace dir — the other tests in this file still cover catalog
324
+ // visibility, which is the core of PR 33.
325
+ if (!workspaceDir) {
326
+ return;
327
+ }
328
+
329
+ const skillsDir = join(workspaceDir, "skills", "shared-id");
330
+ mkdirSync(skillsDir, { recursive: true });
331
+ writeFileSync(
332
+ join(skillsDir, "SKILL.md"),
333
+ [
334
+ "---",
335
+ 'name: "filesystem-version"',
336
+ 'description: "User-authored override"',
337
+ "---",
338
+ "",
339
+ "# Filesystem Override",
340
+ ].join("\n"),
341
+ );
342
+
343
+ const pluginSkill: PluginSkillRegistration = {
344
+ id: "shared-id",
345
+ name: "plugin-version",
346
+ description: "Plugin-provided skill that should be shadowed",
347
+ body: "plugin body",
348
+ };
349
+ registerPlugin(buildSkillPlugin("shadow-plugin", [pluginSkill]));
350
+
351
+ try {
352
+ await bootstrapPlugins(fakeCtx);
353
+
354
+ const catalog = loadSkillCatalog();
355
+ const entry = catalog.find((s) => s.id === "shared-id");
356
+ // The filesystem SKILL.md wins — source flips to "managed" and the
357
+ // plugin's metadata is not what we see.
358
+ expect(entry).toBeDefined();
359
+ expect(entry?.source).toBe("managed");
360
+ expect(entry?.name).toBe("filesystem-version");
361
+ } finally {
362
+ if (existsSync(skillsDir)) {
363
+ rmSync(skillsDir, { recursive: true, force: true });
364
+ }
365
+ }
366
+ });
367
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Tests for plugin tool contributions (PR 31).
3
+ *
4
+ * Covers the end-to-end flow that lets a plugin declare tools on its
5
+ * manifest and have them surface through the global tool registry:
6
+ *
7
+ * - Registering a plugin with `tools: Tool[]`, running `bootstrapPlugins`,
8
+ * and observing the contributed tool via `getAllTools()` / `getTool()`.
9
+ * - Tool ownership metadata (`origin: "plugin"`, `ownerPluginId: <plugin>`)
10
+ * stamped authoritatively by `registerPluginTools` regardless of what the
11
+ * plugin author set on the incoming object.
12
+ * - Shutdown hook unregistering the contributed tools so the registry is
13
+ * clean again after teardown.
14
+ * - Direct `registerPluginTools` / `unregisterPluginTools` semantics,
15
+ * including the plugin-scoped ref count.
16
+ *
17
+ * Uses `mock.module` to stub the credential store so bootstrap doesn't hit
18
+ * the real backend. `resetPluginRegistryForTests()` and
19
+ * `__clearRegistryForTesting()` isolate registry state between cases so
20
+ * this file can run alongside other plugin/tool-registry tests without
21
+ * cross-contamination.
22
+ */
23
+
24
+ import { rm } from "node:fs/promises";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
27
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
28
+
29
+ // Mock the credential store before importing the bootstrap so the module
30
+ // under test captures the stubbed binding. Bootstrap only calls this for
31
+ // plugins that declare `requiresCredential`; the tests in this file don't,
32
+ // so the stub simply returns undefined.
33
+ const getSecureKeyAsyncMock = mock(
34
+ async (_account: string): Promise<string | undefined> => undefined,
35
+ );
36
+ mock.module("../security/secure-keys.js", () => ({
37
+ getSecureKeyAsync: getSecureKeyAsyncMock,
38
+ }));
39
+
40
+ import type { AssistantConfig } from "../config/schema.js";
41
+ import {
42
+ bootstrapPlugins,
43
+ type DaemonContext,
44
+ } from "../daemon/external-plugins-bootstrap.js";
45
+ import { runShutdownHooks } from "../daemon/shutdown-registry.js";
46
+ import { RiskLevel } from "../permissions/types.js";
47
+ import {
48
+ registerPlugin,
49
+ resetPluginRegistryForTests,
50
+ } from "../plugins/registry.js";
51
+ import type { Plugin } from "../plugins/types.js";
52
+ import type { ToolDefinition } from "../providers/types.js";
53
+ import {
54
+ __clearRegistryForTesting,
55
+ __resetRegistryForTesting,
56
+ getAllTools,
57
+ getPluginRefCount,
58
+ getTool,
59
+ registerPluginTools,
60
+ unregisterPluginTools,
61
+ } from "../tools/registry.js";
62
+ import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
63
+
64
+ // Redirect plugin-storage-directory creation into a per-process temp tree so
65
+ // the test doesn't touch the developer's real ~/.vellum. This matches the
66
+ // convention used by plugin-bootstrap.test.ts.
67
+ const TEST_INSTANCE_DIR = join(
68
+ tmpdir(),
69
+ `vellum-plugin-tool-contrib-test-${process.pid}`,
70
+ );
71
+ process.env.BASE_DATA_DIR = TEST_INSTANCE_DIR;
72
+
73
+ const fakeConfig = {} as unknown as AssistantConfig;
74
+ const fakeCtx: DaemonContext = {
75
+ config: fakeConfig,
76
+ assistantVersion: "9.9.9-test",
77
+ };
78
+
79
+ function makeFakeTool(name: string, extras: Partial<Tool> = {}): Tool {
80
+ return {
81
+ name,
82
+ description: `Fake ${name}`,
83
+ category: "plugin-test",
84
+ defaultRiskLevel: RiskLevel.Low,
85
+ getDefinition(): ToolDefinition {
86
+ return {
87
+ name,
88
+ description: `Fake ${name}`,
89
+ input_schema: { type: "object", properties: {}, required: [] },
90
+ };
91
+ },
92
+ async execute(
93
+ _input: Record<string, unknown>,
94
+ _context: ToolContext,
95
+ ): Promise<ToolExecutionResult> {
96
+ return { content: "ok", isError: false };
97
+ },
98
+ ...extras,
99
+ };
100
+ }
101
+
102
+ function buildPlugin(
103
+ name: string,
104
+ extras: Partial<Omit<Plugin, "manifest">> = {},
105
+ ): Plugin {
106
+ return {
107
+ manifest: {
108
+ name,
109
+ version: "0.0.1",
110
+ requires: { pluginRuntime: "v1" },
111
+ },
112
+ ...extras,
113
+ };
114
+ }
115
+
116
+ describe("plugin tool contributions", () => {
117
+ beforeEach(async () => {
118
+ resetPluginRegistryForTests();
119
+ // Clear the tool registry completely so we can make vacuous-free
120
+ // assertions about which tools are present. We don't need any of the
121
+ // eager/host tools for these tests.
122
+ __clearRegistryForTesting();
123
+ getSecureKeyAsyncMock.mockReset();
124
+ getSecureKeyAsyncMock.mockImplementation(async () => undefined);
125
+ await rm(TEST_INSTANCE_DIR, { recursive: true, force: true });
126
+ });
127
+
128
+ test("bootstrap registers plugin tools and makes them discoverable", async () => {
129
+ const tool = makeFakeTool("plugin-contrib-tool");
130
+ const plugin = buildPlugin("alpha-contributor", {
131
+ async init() {},
132
+ tools: [tool],
133
+ });
134
+ registerPlugin(plugin);
135
+
136
+ await bootstrapPlugins(fakeCtx);
137
+
138
+ const retrieved = getTool("plugin-contrib-tool");
139
+ expect(retrieved).toBeDefined();
140
+ // Ownership metadata must be stamped authoritatively by the bootstrap —
141
+ // the registry uses it to drive ref-counting and conflict detection when
142
+ // the plugin shuts down or is hot-reloaded. Plugin tools live in their
143
+ // own `origin: "plugin"` namespace, disjoint from real skills, so a
144
+ // plugin name that happens to match a skill id cannot collide.
145
+ expect(retrieved?.origin).toBe("plugin");
146
+ expect(retrieved?.ownerPluginId).toBe("alpha-contributor");
147
+
148
+ // The tool surfaces in the global `getAllTools()` snapshot, which is
149
+ // what downstream consumers (tool-manifest, session projection) read.
150
+ const names = getAllTools().map((t) => t.name);
151
+ expect(names).toContain("plugin-contrib-tool");
152
+ });
153
+
154
+ test("plugin tools are unregistered when shutdown hooks run", async () => {
155
+ const plugin = buildPlugin("bravo-contributor", {
156
+ async init() {},
157
+ tools: [makeFakeTool("bravo-tool")],
158
+ });
159
+ registerPlugin(plugin);
160
+
161
+ await bootstrapPlugins(fakeCtx);
162
+ expect(getTool("bravo-tool")).toBeDefined();
163
+
164
+ await runShutdownHooks("test-shutdown");
165
+
166
+ expect(getTool("bravo-tool")).toBeUndefined();
167
+ expect(getPluginRefCount("bravo-contributor")).toBe(0);
168
+ });
169
+
170
+ test("bootstrap is a no-op for plugins that declare no tools", async () => {
171
+ const plugin = buildPlugin("no-tools", { async init() {} });
172
+ registerPlugin(plugin);
173
+
174
+ await bootstrapPlugins(fakeCtx);
175
+ // No tool should have been registered.
176
+ expect(getAllTools()).toHaveLength(0);
177
+
178
+ // Shutdown must also be safe — `unregisterPluginTools` is idempotent for
179
+ // plugins that never contributed any tools.
180
+ await runShutdownHooks("test-shutdown");
181
+ expect(getAllTools()).toHaveLength(0);
182
+ });
183
+
184
+ test("tools declared before init() runs are only visible after bootstrap", async () => {
185
+ // Registration alone must not touch the tool registry — only the
186
+ // bootstrap pass does. This matters because `bootstrapPlugins` runs once
187
+ // at daemon startup after the plugin registry is populated; if
188
+ // registration itself contributed tools, hot-reloading a plugin module
189
+ // during boot would race with `initializeTools()`.
190
+ const plugin = buildPlugin("charlie-contributor", {
191
+ async init() {},
192
+ tools: [makeFakeTool("charlie-tool")],
193
+ });
194
+ registerPlugin(plugin);
195
+
196
+ expect(getTool("charlie-tool")).toBeUndefined();
197
+
198
+ await bootstrapPlugins(fakeCtx);
199
+ expect(getTool("charlie-tool")).toBeDefined();
200
+ });
201
+
202
+ test("tools are only registered after init() succeeds", async () => {
203
+ // A plugin whose init throws must not contribute tools — the bootstrap
204
+ // aborts with a PluginExecutionError, and nothing from this plugin
205
+ // should leak into the tool registry.
206
+ const plugin = buildPlugin("delta-broken", {
207
+ async init() {
208
+ throw new Error("boom");
209
+ },
210
+ tools: [makeFakeTool("delta-tool")],
211
+ });
212
+ registerPlugin(plugin);
213
+
214
+ await expect(bootstrapPlugins(fakeCtx)).rejects.toThrow(/delta-broken/);
215
+ expect(getTool("delta-tool")).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ describe("registerPluginTools / unregisterPluginTools helpers", () => {
220
+ beforeEach(() => {
221
+ __resetRegistryForTesting();
222
+ });
223
+
224
+ test("registerPluginTools stamps origin and ownerPluginId from the plugin name", () => {
225
+ // Even if the plugin author hands in a tool with no ownership metadata,
226
+ // the helper fills it in so the tool can be unregistered later.
227
+ const accepted = registerPluginTools("my-plugin", [
228
+ makeFakeTool("pt_stamped"),
229
+ ]);
230
+ expect(accepted).toHaveLength(1);
231
+ expect(accepted[0]?.origin).toBe("plugin");
232
+ expect(accepted[0]?.ownerPluginId).toBe("my-plugin");
233
+
234
+ const retrieved = getTool("pt_stamped");
235
+ expect(retrieved?.origin).toBe("plugin");
236
+ expect(retrieved?.ownerPluginId).toBe("my-plugin");
237
+ });
238
+
239
+ test("registerPluginTools overwrites any pre-existing ownership metadata", () => {
240
+ // A plugin author could (maliciously or mistakenly) hand in a tool
241
+ // pre-tagged with another skill's or plugin's ID. The helper must
242
+ // overwrite it so the bootstrap is always the source of truth for
243
+ // ownership — and it must clear cross-origin fields (ownerSkillId /
244
+ // ownerMcpServerId) so the stamped tool cannot leak across namespaces.
245
+ const spoofed = makeFakeTool("pt_spoof", {
246
+ origin: "skill",
247
+ ownerSkillId: "some-other-skill",
248
+ });
249
+ registerPluginTools("my-plugin", [spoofed]);
250
+ const retrieved = getTool("pt_spoof");
251
+ expect(retrieved?.origin).toBe("plugin");
252
+ expect(retrieved?.ownerPluginId).toBe("my-plugin");
253
+ expect(retrieved?.ownerSkillId).toBeUndefined();
254
+ });
255
+
256
+ test("unregisterPluginTools removes the plugin's tools", () => {
257
+ registerPluginTools("rm-plugin", [
258
+ makeFakeTool("pt_rm_a"),
259
+ makeFakeTool("pt_rm_b"),
260
+ ]);
261
+ expect(getTool("pt_rm_a")).toBeDefined();
262
+ expect(getTool("pt_rm_b")).toBeDefined();
263
+
264
+ unregisterPluginTools("rm-plugin");
265
+
266
+ expect(getTool("pt_rm_a")).toBeUndefined();
267
+ expect(getTool("pt_rm_b")).toBeUndefined();
268
+ });
269
+
270
+ test("unregisterPluginTools is a no-op for plugins that never contributed", () => {
271
+ expect(() => unregisterPluginTools("never-registered")).not.toThrow();
272
+ });
273
+
274
+ test("ref-counting: repeated registrations require matching unregister calls", () => {
275
+ registerPluginTools("rc-plugin", [makeFakeTool("pt_rc")]);
276
+ registerPluginTools("rc-plugin", [makeFakeTool("pt_rc")]);
277
+ expect(getPluginRefCount("rc-plugin")).toBe(2);
278
+
279
+ unregisterPluginTools("rc-plugin");
280
+ expect(getTool("pt_rc")).toBeDefined();
281
+
282
+ unregisterPluginTools("rc-plugin");
283
+ expect(getTool("pt_rc")).toBeUndefined();
284
+ expect(getPluginRefCount("rc-plugin")).toBe(0);
285
+ });
286
+ });