@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Tests for {@link installPlugin}.
3
3
  *
4
- * Two materialization paths are exercised without touching the network or
5
- * spawning real subprocesses:
6
- * - First-party plugins are fetched from the GitHub Contents API, replaced
7
- * by an in-memory fixture passed via the `fetch` dependency.
8
- * - External (whitelisted) plugins are shallow-cloned with `git`, replaced
9
- * by a fake {@link GitRunner} that materializes a tree into the clone dir.
10
- * No globals are monkey-patched and no `--test-hook` exports leak into
11
- * production code.
4
+ * Whitelisted external plugins are shallow-cloned with `git` (replaced by a
5
+ * fake {@link GitRunner} that materializes a tree into the clone dir) and,
6
+ * when a curated adapter stub exists for the name, the stub is overlaid from
7
+ * the GitHub Contents API (replaced by an in-memory fixture passed via the
8
+ * `fetch` dependency). No globals are monkey-patched and no `--test-hook`
9
+ * exports leak into production code.
12
10
  */
13
11
 
14
12
  import {
@@ -45,13 +43,13 @@ const DOWNLOAD_HOST = "https://files.test/";
45
43
  * Build a `fetch` that serves the GitHub Contents API from an in-memory tree.
46
44
  *
47
45
  * `tree` is keyed by full canonical-repo path (e.g.
48
- * `experimental/plugins/simple-memory/package.json`) and maps each entry to a
46
+ * `plugins/caveman/package.json`) and maps each entry to a
49
47
  * file's content (`string`/`Uint8Array`) or `null` for an explicit directory.
50
48
  * Directory listings are derived from the key set; file `download_url`s point
51
49
  * at {@link DOWNLOAD_HOST} and are served with the stored bytes.
52
50
  *
53
51
  * The marketplace manifest lookup is answered with `manifest` (or a 404 when
54
- * omitted, so resolution degrades to the first-party source).
52
+ * omitted, so the name resolves to no installable source).
55
53
  */
56
54
  function makeContentsFetch(opts: {
57
55
  tree: Record<string, Uint8Array | string | null>;
@@ -60,11 +58,11 @@ function makeContentsFetch(opts: {
60
58
  * HTTP status to answer the manifest fetch with. Defaults to 200 (when
61
59
  * `manifest` is provided) or 404 (when omitted). Set to a transient status
62
60
  * (e.g. 500) to simulate a marketplace lookup that fails rather than being
63
- * absent — exercising the degrade-to-first-party path.
61
+ * absent.
64
62
  */
65
63
  manifestStatus?: number;
66
64
  }): FetchLike {
67
- const MANIFEST_URL = `https://api.github.com/repos/${CANON_REPO}/contents/experimental/plugins/marketplace.json`;
65
+ const MANIFEST_URL = `https://api.github.com/repos/${CANON_REPO}/contents/plugins/marketplace.json`;
68
66
  const CONTENTS = `https://api.github.com/repos/${CANON_REPO}/contents/`;
69
67
  const { tree } = opts;
70
68
 
@@ -136,17 +134,6 @@ function makeContentsFetch(opts: {
136
134
  }) as FetchLike;
137
135
  }
138
136
 
139
- /** First-party fixture: keys are relative to `experimental/plugins/`. */
140
- function fixtureFetch(
141
- tree: Record<string, Uint8Array | string | null>,
142
- ): FetchLike {
143
- const full: Record<string, Uint8Array | string | null> = {};
144
- for (const [key, value] of Object.entries(tree)) {
145
- full[`experimental/plugins/${key}`] = value;
146
- }
147
- return makeContentsFetch({ tree: full });
148
- }
149
-
150
137
  /**
151
138
  * Build a fake {@link GitRunner} that simulates a shallow clone.
152
139
  *
@@ -191,21 +178,19 @@ function fakeGitRunner(opts: {
191
178
 
192
179
  /** A git runner that fails the test if any git command is invoked. */
193
180
  const unusedGitRunner: GitRunner = async (args) => {
194
- throw new Error(
195
- `git should not run for a first-party install: ${args.join(" ")}`,
196
- );
181
+ throw new Error(`git should not run for this install: ${args.join(" ")}`);
197
182
  };
198
183
 
199
184
  /**
200
185
  * Read the real, committed caveman adapter stub from the repo so the
201
186
  * integration test exercises the adapter that ships rather than a fixture
202
187
  * copy that could drift from it. Returns every stub file keyed by its
203
- * Contents-API path (`experimental/plugins/caveman/<rel>`) so the fetch fake
188
+ * Contents-API path (`plugins/caveman/<rel>`) so the fetch fake
204
189
  * serves the whole stub — package.json, the adapter, and its templates — to
205
190
  * the real postinstall runner. Resolves the stub relative to this test file.
206
191
  */
207
192
  function readRealCavemanStub(): Record<string, string> {
208
- const repoRel = "experimental/plugins/caveman";
193
+ const repoRel = "plugins/caveman";
209
194
  const stubDir = join(import.meta.dir, "../../../../../", repoRel);
210
195
  const tree: Record<string, string> = {};
211
196
  const walk = (relDir: string): void => {
@@ -223,7 +208,24 @@ function readRealCavemanStub(): Record<string, string> {
223
208
  return tree;
224
209
  }
225
210
 
226
- describe("installPlugin — first-party", () => {
211
+ const CAVEMAN_SHA = "63a91ecadbf4c4719a4602a5abb00883f9966034";
212
+
213
+ const CAVEMAN_MANIFEST = {
214
+ name: "vellum-assistant",
215
+ plugins: [
216
+ {
217
+ name: "caveman",
218
+ source: {
219
+ source: "github",
220
+ repo: "JuliusBrussee/caveman",
221
+ ref: CAVEMAN_SHA,
222
+ },
223
+ description: "Ultra-compressed communication mode.",
224
+ },
225
+ ],
226
+ };
227
+
228
+ describe("installPlugin — install lifecycle", () => {
227
229
  let ws: string;
228
230
  let pluginsDir: string;
229
231
 
@@ -237,280 +239,76 @@ describe("installPlugin — first-party", () => {
237
239
  rmSync(ws, { recursive: true, force: true });
238
240
  });
239
241
 
240
- test("copies a first-party plugin into <workspacePluginsDir>/<name>", async () => {
241
- const result = await installPlugin(
242
- { name: "simple-memory", force: false, ref: "main" },
243
- {
244
- fetch: fixtureFetch({
245
- "simple-memory": null,
246
- "simple-memory/package.json": '{"name":"simple-memory"}',
247
- "simple-memory/README.md": "# simple-memory",
248
- "simple-memory/hooks": null,
249
- "simple-memory/hooks/init.ts": "export default async () => {};\n",
250
- "simple-memory/tools": null,
251
- "simple-memory/tools/ping.ts": "export default {};\n",
252
- }),
253
- workspacePluginsDir: pluginsDir,
254
- },
255
- );
256
-
257
- const target = join(pluginsDir, "simple-memory");
258
- expect(result.target).toBe(target);
259
- expect(result.fileCount).toBe(4);
260
- expect(result.ref).toBe("main");
261
- // First-party installs have no clone, so no commit is resolved.
262
- expect(result.commit).toBeNull();
263
- expect(existsSync(join(target, "package.json"))).toBe(true);
264
- expect(existsSync(join(target, "README.md"))).toBe(true);
265
- expect(existsSync(join(target, "hooks", "init.ts"))).toBe(true);
266
- expect(existsSync(join(target, "tools", "ping.ts"))).toBe(true);
267
- expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
268
- '{"name":"simple-memory"}',
269
- );
270
- });
271
-
272
- test("writes a provenance manifest recording the source and ref", async () => {
273
- // GIVEN a first-party install
274
- const target = join(pluginsDir, "simple-memory");
275
-
276
- // WHEN it completes
277
- await installPlugin(
278
- { name: "simple-memory", force: false, ref: "main" },
279
- {
280
- fetch: fixtureFetch({
281
- "simple-memory/package.json": '{"name":"simple-memory"}',
282
- }),
283
- workspacePluginsDir: pluginsDir,
284
- },
285
- );
286
-
287
- // THEN a hidden manifest records the resolved coordinates (no commit for
288
- // first-party) and is not counted as a plugin file
289
- const manifest = JSON.parse(
290
- readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
291
- );
292
- expect(manifest.name).toBe("simple-memory");
293
- expect(manifest.source.kind).toBe("first-party");
294
- expect(manifest.source.repo).toBe("vellum-assistant");
295
- expect(manifest.source.ref).toBe("main");
296
- expect(manifest.commit).toBeUndefined();
297
- });
298
-
299
242
  test("refuses to overwrite an existing install without --force", async () => {
300
- const target = join(pluginsDir, "simple-memory");
243
+ // GIVEN a plugin already installed at <pluginsDir>/caveman
244
+ const target = join(pluginsDir, "caveman");
301
245
  mkdirSync(target, { recursive: true });
302
- writeFileSync(join(target, "marker"), "pre-existing");
246
+ writeFileSync(join(target, "package.json"), '{"name":"caveman"}');
303
247
 
248
+ // WHEN we install the same name without --force
249
+ // THEN it refuses rather than clobbering the existing copy, and git is
250
+ // never invoked (the guard trips before any clone)
251
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
304
252
  await expect(
305
253
  installPlugin(
306
- { name: "simple-memory", force: false, ref: "main" },
307
- {
308
- fetch: fixtureFetch({
309
- "simple-memory/package.json": "{}",
310
- }),
311
- workspacePluginsDir: pluginsDir,
312
- },
254
+ { name: "caveman", force: false, ref: "main" },
255
+ { fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
313
256
  ),
314
257
  ).rejects.toBeInstanceOf(PluginAlreadyInstalledError);
315
-
316
- // The pre-existing marker is left untouched on refusal.
317
- expect(readFileSync(join(target, "marker"), "utf-8")).toBe("pre-existing");
318
258
  });
319
259
 
320
260
  test("--force replaces an existing install", async () => {
321
- const target = join(pluginsDir, "simple-memory");
261
+ // GIVEN a stale copy already on disk
262
+ const target = join(pluginsDir, "caveman");
322
263
  mkdirSync(target, { recursive: true });
323
- writeFileSync(join(target, "marker"), "pre-existing");
264
+ writeFileSync(join(target, "stale.txt"), "old");
324
265
 
325
- await installPlugin(
326
- { name: "simple-memory", force: true, ref: "main" },
327
- {
328
- fetch: fixtureFetch({
329
- "simple-memory/package.json": '{"name":"simple-memory"}',
330
- }),
331
- workspacePluginsDir: pluginsDir,
332
- },
266
+ // AND a clone that materializes the fresh upstream tree
267
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
268
+ const runGit = fakeGitRunner({
269
+ tree: { "package.json": '{"name":"caveman"}', "README.md": "# caveman" },
270
+ commit: CAVEMAN_SHA,
271
+ });
272
+
273
+ // WHEN we install with --force
274
+ const result = await installPlugin(
275
+ { name: "caveman", force: true, ref: "main" },
276
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
333
277
  );
334
278
 
335
- expect(existsSync(join(target, "marker"))).toBe(false);
279
+ // THEN the fresh tree replaces the old copy entirely
280
+ expect(result.target).toBe(target);
281
+ expect(existsSync(join(target, "stale.txt"))).toBe(false);
336
282
  expect(existsSync(join(target, "package.json"))).toBe(true);
283
+ expect(existsSync(join(target, "README.md"))).toBe(true);
337
284
  });
338
285
 
339
- test("--force preserves the existing install when the fetch fails", async () => {
340
- // A transient 5xx during a forced re-install must NOT delete the
341
- // previously working plugin. The fetch error surfaces, but the existing
342
- // tree on disk is untouched (all writes go through the staging dir).
343
- const target = join(pluginsDir, "simple-memory");
286
+ test("--force preserves the existing install when the clone fails", async () => {
287
+ // GIVEN a working copy already on disk
288
+ const target = join(pluginsDir, "caveman");
344
289
  mkdirSync(target, { recursive: true });
345
- writeFileSync(join(target, "marker"), "pre-existing");
290
+ writeFileSync(join(target, "package.json"), '{"name":"caveman-existing"}');
346
291
 
347
- await expect(
348
- installPlugin(
349
- { name: "simple-memory", force: true, ref: "main" },
350
- {
351
- fetch: (async () =>
352
- new Response("upstream broken", { status: 503 })) as FetchLike,
353
- workspacePluginsDir: pluginsDir,
354
- },
355
- ),
356
- ).rejects.toThrow(/HTTP 503/);
357
-
358
- expect(readFileSync(join(target, "marker"), "utf-8")).toBe("pre-existing");
359
- // And no staging dir leaks into the plugins directory.
360
- expect(readdirSync(pluginsDir)).toEqual(["simple-memory"]);
361
- });
362
-
363
- test("404 on the canonical path is reported as not-found", async () => {
364
- await expect(
365
- installPlugin(
366
- { name: "missing-plugin", force: false, ref: "main" },
367
- {
368
- fetch: fixtureFetch({}),
369
- workspacePluginsDir: pluginsDir,
370
- },
371
- ),
372
- ).rejects.toBeInstanceOf(PluginNotFoundError);
373
-
374
- expect(existsSync(join(pluginsDir, "missing-plugin"))).toBe(false);
375
- // And no staging dir leaks either.
376
- expect(readdirSync(pluginsDir)).toEqual([]);
377
- });
378
-
379
- test("HTTP 5xx from GitHub propagates and leaves no staging behind", async () => {
380
- await expect(
381
- installPlugin(
382
- { name: "demo", force: false, ref: "main" },
383
- {
384
- fetch: (async () =>
385
- new Response("upstream broken", { status: 503 })) as FetchLike,
386
- workspacePluginsDir: pluginsDir,
387
- },
388
- ),
389
- ).rejects.toThrow(/HTTP 503/);
390
-
391
- expect(existsSync(join(pluginsDir, "demo"))).toBe(false);
392
- expect(readdirSync(pluginsDir)).toEqual([]);
393
- });
394
-
395
- test("a rate-limited contents listing surfaces a retryable PluginSourceUnavailableError", async () => {
396
- // GIVEN GitHub's unauthenticated rate limit is exhausted: the contents
397
- // listing 403s with the remaining-quota header at zero
398
- const rateLimited: FetchLike = (async (input: RequestInfo | URL) => {
399
- const url = typeof input === "string" ? input : input.toString();
400
- if (url.includes("marketplace.json")) {
401
- return new Response("not found", { status: 404 });
402
- }
403
- if (url.includes("/contents/")) {
404
- return new Response("rate limited", {
405
- status: 403,
406
- headers: { "x-ratelimit-remaining": "0" },
407
- });
408
- }
409
- return new Response("not found", { status: 404 });
410
- }) as FetchLike;
411
-
412
- // WHEN we install
413
- // THEN the failure is classified as transient (retryable), not a hard
414
- // error, so the route can surface a 503 instead of a 500
415
- await expect(
416
- installPlugin(
417
- { name: "demo", force: false, ref: "main" },
418
- { fetch: rateLimited, workspacePluginsDir: pluginsDir },
419
- ),
420
- ).rejects.toBeInstanceOf(PluginSourceUnavailableError);
421
-
422
- // AND no staging dir leaks behind on the transient failure.
423
- expect(readdirSync(pluginsDir)).toEqual([]);
424
- });
425
-
426
- test("a forbidden contents listing with quota remaining stays a hard error", async () => {
427
- // GIVEN a 403 that is NOT a rate-limit (quota header present and nonzero):
428
- // a genuine authorization failure, not a transient one
429
- const forbidden: FetchLike = (async (input: RequestInfo | URL) => {
430
- const url = typeof input === "string" ? input : input.toString();
431
- if (url.includes("marketplace.json")) {
432
- return new Response("not found", { status: 404 });
433
- }
434
- if (url.includes("/contents/")) {
435
- return new Response("forbidden", {
436
- status: 403,
437
- headers: { "x-ratelimit-remaining": "57" },
438
- });
439
- }
440
- return new Response("not found", { status: 404 });
441
- }) as FetchLike;
292
+ // AND a clone that fails mid-fetch
293
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
294
+ const runGit = fakeGitRunner({
295
+ tree: {},
296
+ fetchError: Object.assign(new Error("git fetch failed"), {
297
+ stderr: "fatal: unable to access: Could not resolve host: github.com",
298
+ }),
299
+ });
442
300
 
443
- // WHEN we install
444
- // THEN it surfaces as a hard error — NOT the retryable variant — so the
445
- // route maps it to 500 rather than inviting an endless retry loop
301
+ // WHEN we install with --force and the fetch fails
302
+ // THEN the previous install is left untouched (staging never swapped in)
446
303
  await expect(
447
304
  installPlugin(
448
- { name: "demo", force: false, ref: "main" },
449
- { fetch: forbidden, workspacePluginsDir: pluginsDir },
305
+ { name: "caveman", force: true, ref: "main" },
306
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
450
307
  ),
451
- ).rejects.not.toBeInstanceOf(PluginSourceUnavailableError);
452
- expect(readdirSync(pluginsDir)).toEqual([]);
453
- });
454
-
455
- test("respects ref by forwarding it to the contents listing", async () => {
456
- // The requested ref must reach the first-party contents listing request.
457
- let listRef: string | undefined;
458
- const base = fixtureFetch({ "demo/package.json": "{}" });
459
- const fetch: FetchLike = async (input, init) => {
460
- const url = typeof input === "string" ? input : input.toString();
461
- const match = /\/contents\/experimental\/plugins\/demo\?ref=([^&]+)/.exec(
462
- url,
463
- );
464
- if (match) listRef = decodeURIComponent(match[1]!);
465
- return base(url, init);
466
- };
467
-
468
- await installPlugin(
469
- { name: "demo", force: false, ref: "feat-branch" },
470
- { fetch, workspacePluginsDir: pluginsDir },
308
+ ).rejects.toThrow();
309
+ expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
310
+ '{"name":"caveman-existing"}',
471
311
  );
472
-
473
- expect(listRef).toBe("feat-branch");
474
- expect(existsSync(join(pluginsDir, "demo", "package.json"))).toBe(true);
475
- });
476
-
477
- test("rejects untrusted entry names from the GitHub response", async () => {
478
- // Even though GitHub returns trustworthy data, defense-in-depth requires
479
- // us to validate every entry name before any filesystem write. A malicious
480
- // or buggy upstream that hands us `../escape` must not write outside the
481
- // target.
482
- const badFetch: FetchLike = (async (input: RequestInfo | URL) => {
483
- const url = typeof input === "string" ? input : input.toString();
484
- if (url.includes("marketplace.json")) {
485
- return new Response("not found", { status: 404 });
486
- }
487
- if (url.includes("/contents/")) {
488
- return new Response(
489
- JSON.stringify([
490
- {
491
- name: "../escape",
492
- path: "experimental/plugins/demo/../escape",
493
- type: "file",
494
- download_url: `${DOWNLOAD_HOST}escape`,
495
- },
496
- ]),
497
- { status: 200 },
498
- );
499
- }
500
- return new Response("x", { status: 200 });
501
- }) as FetchLike;
502
-
503
- await expect(
504
- installPlugin(
505
- { name: "demo", force: false, ref: "main" },
506
- { fetch: badFetch, workspacePluginsDir: pluginsDir },
507
- ),
508
- ).rejects.toThrow(/Unsafe entry name/);
509
-
510
- // Nothing was written outside the target — in fact, the target itself
511
- // is gone because the failed install rolled back the staging dir.
512
- expect(existsSync(join(pluginsDir, "..", "escape"))).toBe(false);
513
- expect(readdirSync(pluginsDir)).toEqual([]);
514
312
  });
515
313
  });
516
314
 
@@ -528,21 +326,6 @@ describe("installPlugin — marketplace resolution", () => {
528
326
  rmSync(ws, { recursive: true, force: true });
529
327
  });
530
328
 
531
- const CAVEMAN_MANIFEST = {
532
- name: "vellum-assistant",
533
- plugins: [
534
- {
535
- name: "caveman",
536
- source: {
537
- source: "github",
538
- repo: "JuliusBrussee/caveman",
539
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
540
- },
541
- description: "Ultra-compressed communication mode.",
542
- },
543
- ],
544
- };
545
-
546
329
  test("installs a whitelisted plugin by shallow-cloning its pinned repo + ref", async () => {
547
330
  // GIVEN a marketplace whitelisting caveman at its repo root, pinned to a commit SHA
548
331
  const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
@@ -588,11 +371,11 @@ describe("installPlugin — marketplace resolution", () => {
588
371
  "https://github.com/JuliusBrussee/caveman.git",
589
372
  );
590
373
 
591
- // AND a provenance manifest records the external source + commit.
374
+ // AND a provenance manifest records the github source + commit.
592
375
  const manifest = JSON.parse(
593
376
  readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
594
377
  );
595
- expect(manifest.source.kind).toBe("external");
378
+ expect(manifest.source.kind).toBe("github");
596
379
  expect(manifest.source.owner).toBe("JuliusBrussee");
597
380
  expect(manifest.source.ref).toBe(
598
381
  "63a91ecadbf4c4719a4602a5abb00883f9966034",
@@ -664,14 +447,13 @@ describe("installPlugin — marketplace resolution", () => {
664
447
  expect(readdirSync(pluginsDir)).toEqual([]);
665
448
  });
666
449
 
667
- test("a name absent from the manifest falls back to the first-party source", async () => {
668
- // GIVEN a manifest that does NOT whitelist "simple-memory", and no such
669
- // first-party tree in the canonical repo
450
+ test("a name absent from the manifest is a not-found", async () => {
451
+ // GIVEN a manifest that does NOT whitelist "simple-memory"
670
452
  const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
671
453
 
672
- // WHEN we install a first-party name
673
- // THEN resolution falls back to the first-party path and surfaces a clean
674
- // not-found pointing at the first-party source (git is never invoked)
454
+ // WHEN we install an unlisted name
455
+ // THEN it is a clean not-found pointing at the marketplace manifest, and
456
+ // git is never invoked (there is no source to clone)
675
457
  await expect(
676
458
  installPlugin(
677
459
  { name: "simple-memory", ref: "main" },
@@ -679,13 +461,13 @@ describe("installPlugin — marketplace resolution", () => {
679
461
  ),
680
462
  ).rejects.toMatchObject({
681
463
  constructor: PluginNotFoundError,
682
- message: expect.stringContaining("vellum-ai/vellum-assistant"),
464
+ message: expect.stringContaining("plugins/marketplace.json"),
683
465
  });
684
466
  });
685
467
 
686
468
  test("overlays a curated adapter stub and runs its postinstall transform", async () => {
687
469
  // GIVEN caveman is whitelisted externally
688
- // AND we curate an adapter stub for it at experimental/plugins/caveman —
470
+ // AND we curate an adapter stub for it at plugins/caveman —
689
471
  // the real, committed stub (package.json + postinstall.ts + templates), so
690
472
  // this test exercises the adapter that ships, not a fixture copy of it
691
473
  const fetch = makeContentsFetch({
@@ -697,7 +479,12 @@ describe("installPlugin — marketplace resolution", () => {
697
479
  // terse-mode ruleset in skills/caveman/SKILL.md
698
480
  const runGit = fakeGitRunner({
699
481
  tree: {
700
- "package.json": '{"name":"caveman-installer"}',
482
+ "package.json": JSON.stringify({
483
+ name: "caveman-installer",
484
+ version: "0.1.0",
485
+ description: "Caveman installer.",
486
+ license: "MIT",
487
+ }),
701
488
  ".claude-plugin/plugin.json": JSON.stringify({
702
489
  name: "caveman",
703
490
  description: "Ultra-compressed communication mode.",
@@ -723,6 +510,12 @@ describe("installPlugin — marketplace resolution", () => {
723
510
  expect(pkg.name).toBe("caveman");
724
511
  expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeString();
725
512
  expect(pkg.scripts?.postinstall).toBeUndefined();
513
+ // AND every other upstream manifest field is preserved verbatim — only
514
+ // `name` and the peer dep are touched, so the installed plugin reports the
515
+ // upstream `version` (and description, license, …) rather than the stub's
516
+ expect(pkg.version).toBe("0.1.0");
517
+ expect(pkg.description).toBe("Caveman installer.");
518
+ expect(pkg.license).toBe("MIT");
726
519
 
727
520
  // AND a pre-model-call hook is synthesized carrying the upstream ruleset,
728
521
  // so caveman actually runs (terse mode injected on the user-facing call)
@@ -737,9 +530,156 @@ describe("installPlugin — marketplace resolution", () => {
737
530
  expect(hook).not.toContain("description: terse mode");
738
531
  });
739
532
 
533
+ test("falls back to the stub manifest when the upstream ships no package.json", async () => {
534
+ // GIVEN caveman is whitelisted with the real curated adapter stub
535
+ const fetch = makeContentsFetch({
536
+ tree: readRealCavemanStub(),
537
+ manifest: CAVEMAN_MANIFEST,
538
+ });
539
+ // AND the upstream clone has NO package.json — only the Claude Code
540
+ // manifest and the terse-mode ruleset the adapter reads
541
+ const runGit = fakeGitRunner({
542
+ tree: {
543
+ ".claude-plugin/plugin.json": JSON.stringify({ name: "caveman" }),
544
+ "skills/caveman/SKILL.md":
545
+ "---\nname: caveman\n---\n\nCAVEMAN MODE. Drop filler words.",
546
+ },
547
+ commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
548
+ });
549
+
550
+ // WHEN we install (real postinstall runner)
551
+ await installPlugin(
552
+ { name: "caveman", ref: "main" },
553
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
554
+ );
555
+
556
+ // THEN the overlaid stub is the only manifest available, so it becomes the
557
+ // base: the loader still gets a matching `name` and the peer dep, and the
558
+ // spent stub `postinstall` is dropped so no install machinery is shipped
559
+ const pkg = JSON.parse(
560
+ readFileSync(join(pluginsDir, "caveman", "package.json"), "utf-8"),
561
+ );
562
+ expect(pkg.name).toBe("caveman");
563
+ expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeString();
564
+ expect(pkg.scripts?.postinstall).toBeUndefined();
565
+ });
566
+
567
+ test("strips a clone-supplied bunfig.toml so its preload can't run before the adapter", async () => {
568
+ // GIVEN caveman is whitelisted with the real, committed adapter stub
569
+ const fetch = makeContentsFetch({
570
+ tree: readRealCavemanStub(),
571
+ manifest: CAVEMAN_MANIFEST,
572
+ });
573
+ // AND the upstream clone is a valid Claude Code plugin that ALSO smuggles a
574
+ // `bunfig.toml` whose `preload` points at attacker code in the clone. Bun
575
+ // loads `$cwd/bunfig.toml` and runs `preload` before the entry point, so an
576
+ // un-stripped config would execute `evil.ts` (writing a `pwned` sentinel)
577
+ // ahead of the curated adapter — arbitrary code execution.
578
+ const runGit = fakeGitRunner({
579
+ tree: {
580
+ "package.json": '{"name":"caveman-installer"}',
581
+ ".claude-plugin/plugin.json": JSON.stringify({
582
+ name: "caveman",
583
+ description: "Ultra-compressed communication mode.",
584
+ }),
585
+ "skills/caveman/SKILL.md":
586
+ "---\nname: caveman\ndescription: terse mode\n---\n\nCAVEMAN MODE. Drop filler words. Keep technical substance.",
587
+ "bunfig.toml": 'preload = ["./evil.ts"]\n',
588
+ "evil.ts":
589
+ 'import { writeFileSync } from "node:fs";\nwriteFileSync("pwned", "pwned");\n',
590
+ },
591
+ commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
592
+ });
593
+
594
+ // WHEN we install with the real postinstall runner (real `bun` subprocess)
595
+ const result = await installPlugin(
596
+ { name: "caveman", ref: "main" },
597
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
598
+ );
599
+
600
+ // THEN the adapter still ran and produced a valid Vellum plugin
601
+ const target = join(pluginsDir, "caveman");
602
+ expect(result.commit).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
603
+ expect(
604
+ readFileSync(join(target, "hooks", "pre-model-call.ts"), "utf-8"),
605
+ ).toContain("CAVEMAN MODE. Drop filler words.");
606
+ // AND the upstream bunfig.toml was dropped before `bun` ran, so its preload
607
+ // never fired — no sentinel was written — and the config never persists in
608
+ // the installed plugin
609
+ expect(existsSync(join(target, "pwned"))).toBe(false);
610
+ expect(existsSync(join(target, "bunfig.toml"))).toBe(false);
611
+ });
612
+
613
+ test("drops a clone-supplied bunfig.toml regardless of filename case", async () => {
614
+ // GIVEN a raw external clone (no adapter stub) that smuggles a Bun config
615
+ // under an uppercase name. The macOS install target is case-insensitive, so
616
+ // Bun would still open a `BUNFIG.TOML`; a case-sensitive skip would miss it.
617
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
618
+ const runGit = fakeGitRunner({
619
+ tree: {
620
+ "package.json": '{"name":"caveman"}',
621
+ "BUNFIG.TOML": 'preload = ["./evil.ts"]\n',
622
+ },
623
+ commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
624
+ });
625
+
626
+ // WHEN we install
627
+ await installPlugin(
628
+ { name: "caveman", ref: "main" },
629
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
630
+ );
631
+
632
+ // THEN the uppercase Bun config was dropped while the plugin's own files
633
+ // were materialized
634
+ const target = join(pluginsDir, "caveman");
635
+ expect(existsSync(join(target, "BUNFIG.TOML"))).toBe(false);
636
+ expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
637
+ '{"name":"caveman"}',
638
+ );
639
+ });
640
+
641
+ test("stages the clone outside the served plugins/ directory", async () => {
642
+ // GIVEN caveman is whitelisted externally (raw clone, no adapter stub)
643
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
644
+ // AND a git runner that records the working directory it clones into
645
+ const cloneCwds: string[] = [];
646
+ const runGit: GitRunner = async (args, { cwd }) => {
647
+ if (args[0] === "fetch") {
648
+ cloneCwds.push(cwd);
649
+ mkdirSync(join(cwd, ".git"), { recursive: true });
650
+ writeFileSync(join(cwd, "package.json"), '{"name":"caveman"}');
651
+ return { stdout: "" };
652
+ }
653
+ if (args[0] === "rev-parse") {
654
+ return { stdout: "63a91ecadbf4c4719a4602a5abb00883f9966034\n" };
655
+ }
656
+ return { stdout: "" };
657
+ };
658
+
659
+ // WHEN we install
660
+ const result = await installPlugin(
661
+ { name: "caveman", ref: "main" },
662
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
663
+ );
664
+
665
+ // THEN the half-built clone was staged in a sibling directory, never inside
666
+ // the served plugins/ tree — so the daemon's source watcher and startup
667
+ // loader can't observe the un-adapted clone (wrong name, no peer dep) and
668
+ // log spurious name-mismatch / missing-peer-dependency warnings mid-install
669
+ expect(cloneCwds).toHaveLength(1);
670
+ expect(dirname(cloneCwds[0]!)).toBe(
671
+ join(dirname(pluginsDir), ".plugins-staging"),
672
+ );
673
+
674
+ // AND the finished plugin still lands inside plugins/, with no staging
675
+ // artifact left behind in the served directory
676
+ expect(result.target).toBe(join(pluginsDir, "caveman"));
677
+ expect(readdirSync(pluginsDir)).toEqual(["caveman"]);
678
+ });
679
+
740
680
  test("does not run a postinstall for a raw clone without an adapter stub", async () => {
741
681
  // GIVEN caveman is whitelisted but we curate NO adapter stub for it
742
- // (the Contents API has no experimental/plugins/caveman directory)
682
+ // (the Contents API has no plugins/caveman directory)
743
683
  const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
744
684
  const runGit = fakeGitRunner({
745
685
  tree: { "package.json": '{"name":"caveman"}', "hooks/init.ts": "//" },
@@ -767,11 +707,11 @@ describe("installPlugin — marketplace resolution", () => {
767
707
  // GIVEN a whitelisted plugin with a curated stub declaring a postinstall
768
708
  const fetch = makeContentsFetch({
769
709
  tree: {
770
- "experimental/plugins/caveman/package.json": JSON.stringify({
710
+ "plugins/caveman/package.json": JSON.stringify({
771
711
  name: "caveman",
772
712
  scripts: { postinstall: "bun ./postinstall.ts" },
773
713
  }),
774
- "experimental/plugins/caveman/postinstall.ts": "// adapter",
714
+ "plugins/caveman/postinstall.ts": "// adapter",
775
715
  },
776
716
  manifest: CAVEMAN_MANIFEST,
777
717
  });
@@ -801,7 +741,7 @@ describe("installPlugin — marketplace resolution", () => {
801
741
  // rather than the supported `bun <script>` adapter convention
802
742
  const fetch = makeContentsFetch({
803
743
  tree: {
804
- "experimental/plugins/caveman/package.json": JSON.stringify({
744
+ "plugins/caveman/package.json": JSON.stringify({
805
745
  name: "caveman",
806
746
  scripts: { postinstall: "rm -rf /" },
807
747
  }),
@@ -825,33 +765,21 @@ describe("installPlugin — marketplace resolution", () => {
825
765
  expect(readdirSync(pluginsDir)).toEqual([]);
826
766
  });
827
767
 
828
- test("refuses to install an adapter stub as first-party when the marketplace lookup fails", async () => {
768
+ test("a transient marketplace failure surfaces a retryable error", async () => {
829
769
  // GIVEN the marketplace lookup fails transiently (e.g. rate-limit / 5xx)
830
- // rather than being absent, so resolution degrades to the first-party path
831
- // AND the same-named in-repo directory is an adapter stub (a package.json
832
- // with a postinstall + adapter script, but no hooks/tools of its own)
833
- const fetch = makeContentsFetch({
834
- tree: {
835
- "experimental/plugins/caveman/package.json": JSON.stringify({
836
- name: "caveman",
837
- scripts: { postinstall: "bun ./postinstall.ts" },
838
- }),
839
- "experimental/plugins/caveman/postinstall.ts": "// adapter",
840
- },
841
- manifestStatus: 500,
842
- });
770
+ // rather than being absent, so the name can't be resolved to a source
771
+ const fetch = makeContentsFetch({ tree: {}, manifestStatus: 500 });
843
772
 
844
- // WHEN we install the name (git never runs — there is no external source to
773
+ // WHEN we install the name (git never runs — there is no resolved source to
845
774
  // clone because the marketplace that would have named it was unreadable)
846
- // THEN it fails loudly with a PluginPostinstallError instead of
847
- // materializing the bare stub as a non-functional standalone plugin, and
848
- // nothing is left behind on disk
775
+ // THEN it surfaces as the retryable variant so the route maps it to 503,
776
+ // and nothing is left behind on disk
849
777
  await expect(
850
778
  installPlugin(
851
779
  { name: "caveman", ref: "main" },
852
780
  { fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
853
781
  ),
854
- ).rejects.toBeInstanceOf(PluginPostinstallError);
782
+ ).rejects.toBeInstanceOf(PluginSourceUnavailableError);
855
783
  expect(readdirSync(pluginsDir)).toEqual([]);
856
784
  });
857
785
  });