@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
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Tests for the conversation surface API — the explicit opt-in that promotes
3
+ * a background/scheduled conversation into the default ("standard")
4
+ * conversation listing so clients show it in the Recents sidebar grouping.
5
+ *
6
+ * Exercises:
7
+ * - POST /v1/conversations/:id/surface (set + clear, validation, 404)
8
+ * - GET /v1/conversations default listing inclusion of surfaced rows
9
+ * - GET /v1/conversations?conversationType=background|scheduled unchanged
10
+ */
11
+
12
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ mock.module("../../../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ }));
20
+
21
+ // Stub the event hub to avoid spinning up real SSE infrastructure.
22
+ mock.module("../../assistant-event-hub.js", () => ({
23
+ assistantEventHub: {
24
+ publish: async () => {},
25
+ subscribe: () => () => {},
26
+ },
27
+ broadcastMessage: () => {},
28
+ }));
29
+
30
+ // Spy on the sync publish so we can assert the invalidation fires after a
31
+ // successful write without standing up the real broadcast machinery.
32
+ const publishedListAndMetadata: Array<{
33
+ reason: string;
34
+ conversationIds: string | string[];
35
+ }> = [];
36
+ mock.module("../../sync/resource-sync-events.js", () => ({
37
+ publishConversationListChanged: () => {},
38
+ publishConversationTitleChanged: () => {},
39
+ publishConversationListAndMetadataChanged: (
40
+ reason: string,
41
+ conversationIds: string | string[],
42
+ ) => {
43
+ publishedListAndMetadata.push({ reason, conversationIds });
44
+ },
45
+ }));
46
+
47
+ import {
48
+ createConversation,
49
+ getConversation,
50
+ } from "../../../memory/conversation-crud.js";
51
+ import { getDb } from "../../../memory/db-connection.js";
52
+ import { initializeDb } from "../../../memory/db-init.js";
53
+ import { conversations } from "../../../memory/schema.js";
54
+ import { ROUTES as CONVERSATION_LIST_ROUTES } from "../conversation-list-routes.js";
55
+ import { ROUTES as CONVERSATION_MANAGEMENT_ROUTES } from "../conversation-management-routes.js";
56
+ import { BadRequestError, NotFoundError } from "../errors.js";
57
+ import type { RouteDefinition } from "../types.js";
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // DB bootstrap
61
+ // ---------------------------------------------------------------------------
62
+
63
+ initializeDb();
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function findHandler(routes: RouteDefinition[], operationId: string) {
70
+ const route = routes.find((r) => r.operationId === operationId);
71
+ if (!route) throw new Error(`Route ${operationId} not found`);
72
+ return route.handler;
73
+ }
74
+
75
+ const surfaceHandler = findHandler(
76
+ CONVERSATION_MANAGEMENT_ROUTES,
77
+ "surfaceConversation",
78
+ );
79
+ const listHandler = findHandler(CONVERSATION_LIST_ROUTES, "listConversations");
80
+ const reorderHandler = findHandler(
81
+ CONVERSATION_MANAGEMENT_ROUTES,
82
+ "reorderConversations",
83
+ );
84
+
85
+ interface SurfaceResponse {
86
+ ok: boolean;
87
+ conversationId: string;
88
+ surfacedAt: number | null;
89
+ }
90
+
91
+ interface ConversationSummary {
92
+ id: string;
93
+ title: string;
94
+ conversationType: string;
95
+ surfacedAt?: number;
96
+ }
97
+
98
+ interface ListResponse {
99
+ conversations: ConversationSummary[];
100
+ nextOffset: number;
101
+ hasMore: boolean;
102
+ }
103
+
104
+ function clearConversations(): void {
105
+ getDb().delete(conversations).run();
106
+ }
107
+
108
+ function seed(
109
+ title: string,
110
+ conversationType: "standard" | "background" | "scheduled",
111
+ ): string {
112
+ return createConversation({ title, conversationType, source: "test" }).id;
113
+ }
114
+
115
+ function listIds(queryParams: Record<string, string> = {}): string[] {
116
+ const response = listHandler({ queryParams }) as ListResponse;
117
+ return response.conversations.map((c) => c.id);
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Tests
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe("POST /v1/conversations/:id/surface", () => {
125
+ beforeEach(() => {
126
+ clearConversations();
127
+ publishedListAndMetadata.length = 0;
128
+ });
129
+
130
+ test("surfaces a background conversation and returns the timestamp", () => {
131
+ const id = seed("Background run", "background");
132
+ const before = Date.now();
133
+ const response = surfaceHandler({
134
+ pathParams: { id },
135
+ body: { surfaced: true },
136
+ }) as SurfaceResponse;
137
+ expect(response.ok).toBe(true);
138
+ expect(response.conversationId).toBe(id);
139
+ expect(response.surfacedAt).toBeGreaterThanOrEqual(before);
140
+ });
141
+
142
+ test("publishes a shape-changing list invalidation after the write", () => {
143
+ const id = seed("Background run", "background");
144
+ surfaceHandler({ pathParams: { id }, body: { surfaced: true } });
145
+ expect(publishedListAndMetadata).toEqual([
146
+ { reason: "reordered", conversationIds: id },
147
+ ]);
148
+ });
149
+
150
+ test("clearing returns surfacedAt null", () => {
151
+ const id = seed("Background run", "background");
152
+ surfaceHandler({ pathParams: { id }, body: { surfaced: true } });
153
+ const response = surfaceHandler({
154
+ pathParams: { id },
155
+ body: { surfaced: false },
156
+ }) as SurfaceResponse;
157
+ expect(response.surfacedAt).toBeNull();
158
+ });
159
+
160
+ test("404s for an unknown conversation id", () => {
161
+ expect(() =>
162
+ surfaceHandler({
163
+ pathParams: { id: crypto.randomUUID() },
164
+ body: { surfaced: true },
165
+ }),
166
+ ).toThrow(NotFoundError);
167
+ });
168
+
169
+ test("400s when surfaced is missing or not a boolean", () => {
170
+ const id = seed("Background run", "background");
171
+ expect(() => surfaceHandler({ pathParams: { id }, body: {} })).toThrow(
172
+ BadRequestError,
173
+ );
174
+ expect(() =>
175
+ surfaceHandler({ pathParams: { id }, body: { surfaced: "yes" } }),
176
+ ).toThrow(BadRequestError);
177
+ });
178
+ });
179
+
180
+ describe("GET /v1/conversations — surfaced rows in the default listing", () => {
181
+ beforeEach(() => {
182
+ clearConversations();
183
+ publishedListAndMetadata.length = 0;
184
+ });
185
+
186
+ test("background/scheduled rows stay out of the default listing until surfaced", () => {
187
+ const standardId = seed("Standard chat", "standard");
188
+ seed("Background run", "background");
189
+ seed("Scheduled run", "scheduled");
190
+ expect(listIds()).toEqual([standardId]);
191
+ });
192
+
193
+ test("surfacing promotes a background conversation into the default listing", () => {
194
+ const standardId = seed("Standard chat", "standard");
195
+ const backgroundId = seed("Background run", "background");
196
+ surfaceHandler({
197
+ pathParams: { id: backgroundId },
198
+ body: { surfaced: true },
199
+ });
200
+
201
+ const response = listHandler({ queryParams: {} }) as ListResponse;
202
+ expect(response.conversations.map((c) => c.id).sort()).toEqual(
203
+ [standardId, backgroundId].sort(),
204
+ );
205
+ const surfaced = response.conversations.find((c) => c.id === backgroundId)!;
206
+ expect(surfaced.conversationType).toBe("background");
207
+ expect(surfaced.surfacedAt).toBeGreaterThan(0);
208
+ const standard = response.conversations.find((c) => c.id === standardId)!;
209
+ expect(standard.surfacedAt).toBeUndefined();
210
+ });
211
+
212
+ test("surfacing promotes a scheduled conversation into the default listing", () => {
213
+ const scheduledId = seed("Scheduled run", "scheduled");
214
+ surfaceHandler({
215
+ pathParams: { id: scheduledId },
216
+ body: { surfaced: true },
217
+ });
218
+ expect(listIds()).toEqual([scheduledId]);
219
+ });
220
+
221
+ test("clearing the promotion removes the row from the default listing again", () => {
222
+ const backgroundId = seed("Background run", "background");
223
+ surfaceHandler({
224
+ pathParams: { id: backgroundId },
225
+ body: { surfaced: true },
226
+ });
227
+ expect(listIds()).toEqual([backgroundId]);
228
+
229
+ surfaceHandler({
230
+ pathParams: { id: backgroundId },
231
+ body: { surfaced: false },
232
+ });
233
+ expect(listIds()).toEqual([]);
234
+ });
235
+
236
+ test("filtered background/scheduled listings keep returning surfaced rows", () => {
237
+ const backgroundId = seed("Background run", "background");
238
+ const scheduledId = seed("Scheduled run", "scheduled");
239
+ surfaceHandler({
240
+ pathParams: { id: backgroundId },
241
+ body: { surfaced: true },
242
+ });
243
+ surfaceHandler({
244
+ pathParams: { id: scheduledId },
245
+ body: { surfaced: true },
246
+ });
247
+
248
+ // "background" is the back-compat umbrella (background + scheduled).
249
+ expect(listIds({ conversationType: "background" }).sort()).toEqual(
250
+ [backgroundId, scheduledId].sort(),
251
+ );
252
+ expect(listIds({ conversationType: "scheduled" })).toEqual([scheduledId]);
253
+ });
254
+
255
+ test("moving a surfaced conversation to system:background clears the promotion", () => {
256
+ const backgroundId = seed("Background run", "background");
257
+ surfaceHandler({
258
+ pathParams: { id: backgroundId },
259
+ body: { surfaced: true },
260
+ });
261
+ expect(listIds()).toEqual([backgroundId]);
262
+
263
+ // Move-to-Group sends only groupId/isPinned through the reorder
264
+ // endpoint — the demotion out of Recents must happen in the same
265
+ // server-side write, with no second API call from the client.
266
+ reorderHandler({
267
+ body: {
268
+ updates: [
269
+ { conversationId: backgroundId, groupId: "system:background" },
270
+ ],
271
+ },
272
+ });
273
+
274
+ expect(getConversation(backgroundId)?.surfacedAt).toBeNull();
275
+ expect(listIds()).toEqual([]);
276
+ expect(listIds({ conversationType: "background" })).toEqual([backgroundId]);
277
+ });
278
+
279
+ test("moving a surfaced conversation to system:scheduled clears the promotion", () => {
280
+ const scheduledId = seed("Scheduled run", "scheduled");
281
+ surfaceHandler({
282
+ pathParams: { id: scheduledId },
283
+ body: { surfaced: true },
284
+ });
285
+ expect(listIds()).toEqual([scheduledId]);
286
+
287
+ reorderHandler({
288
+ body: {
289
+ updates: [{ conversationId: scheduledId, groupId: "system:scheduled" }],
290
+ },
291
+ });
292
+
293
+ expect(getConversation(scheduledId)?.surfacedAt).toBeNull();
294
+ expect(listIds()).toEqual([]);
295
+ });
296
+
297
+ test("moving a surfaced conversation to a non-demoting group keeps the promotion", () => {
298
+ const backgroundId = seed("Background run", "background");
299
+ surfaceHandler({
300
+ pathParams: { id: backgroundId },
301
+ body: { surfaced: true },
302
+ });
303
+
304
+ reorderHandler({
305
+ body: {
306
+ updates: [{ conversationId: backgroundId, groupId: "system:all" }],
307
+ },
308
+ });
309
+
310
+ expect(getConversation(backgroundId)?.surfacedAt).toBeGreaterThan(0);
311
+ });
312
+
313
+ test("archived surfaced rows stay out of the default (active) listing", () => {
314
+ const backgroundId = seed("Background run", "background");
315
+ surfaceHandler({
316
+ pathParams: { id: backgroundId },
317
+ body: { surfaced: true },
318
+ });
319
+ getDb().update(conversations).set({ archivedAt: Date.now() }).run();
320
+ expect(listIds()).toEqual([]);
321
+ });
322
+ });
@@ -29,7 +29,6 @@ mock.module("../../../util/logger.js", () => ({
29
29
 
30
30
  // ── Real imports (after mocks) ────────────────────────────────────────────────
31
31
 
32
- import { setOverridesForTesting } from "../../../__tests__/feature-flag-test-helpers.js";
33
32
  import { getDb } from "../../../memory/db-connection.js";
34
33
  import { initializeDb } from "../../../memory/db-init.js";
35
34
  import { providerConnections } from "../../../memory/schema/inference.js";
@@ -91,7 +90,6 @@ function seedConnection(opts: {
91
90
  beforeEach(() => {
92
91
  clearConnections();
93
92
  fakeConfig = {};
94
- setOverridesForTesting({ "openai-compatible-endpoints": true });
95
93
  });
96
94
 
97
95
  // ── GET list ─────────────────────────────────────────────────────────────────
@@ -160,36 +158,6 @@ describe("GET inference/provider-connections (list)", () => {
160
158
  expect(result.connections).toEqual([]);
161
159
  });
162
160
 
163
- test("hides openai-compatible rows when the feature flag is disabled", async () => {
164
- setOverridesForTesting({ "openai-compatible-endpoints": false });
165
- seedConnection({
166
- name: "my-openai",
167
- provider: "openai",
168
- auth: { type: "platform" },
169
- });
170
- seedConnection({
171
- name: "my-compatible",
172
- provider: "openai-compatible",
173
- auth: { type: "api_key", credential: "ref/k" },
174
- });
175
-
176
- const result = (await call(
177
- findHandler("inference_provider_connections_list"),
178
- {},
179
- )) as { connections: Array<{ name: string }> };
180
-
181
- expect(result.connections.map((c) => c.name)).toEqual(["my-openai"]);
182
- });
183
-
184
- test("rejects openai-compatible provider filter when the feature flag is disabled", async () => {
185
- setOverridesForTesting({ "openai-compatible-endpoints": false });
186
-
187
- await expect(
188
- call(findHandler("inference_provider_connections_list"), {
189
- queryParams: { provider: "openai-compatible" },
190
- }),
191
- ).rejects.toBeInstanceOf(BadRequestError);
192
- });
193
161
  });
194
162
 
195
163
  // ── GET single ────────────────────────────────────────────────────────────────
@@ -219,20 +187,6 @@ describe("GET inference/provider-connections/:name (single)", () => {
219
187
  ).rejects.toBeInstanceOf(NotFoundError);
220
188
  });
221
189
 
222
- test("throws 404 for openai-compatible row when the feature flag is disabled", async () => {
223
- setOverridesForTesting({ "openai-compatible-endpoints": false });
224
- seedConnection({
225
- name: "my-compatible",
226
- provider: "openai-compatible",
227
- auth: { type: "api_key", credential: "ref/k" },
228
- });
229
-
230
- await expect(
231
- call(findHandler("inference_provider_connections_get"), {
232
- pathParams: { name: "my-compatible" },
233
- }),
234
- ).rejects.toBeInstanceOf(NotFoundError);
235
- });
236
190
  });
237
191
 
238
192
  // ── POST create ───────────────────────────────────────────────────────────────
@@ -317,22 +271,6 @@ describe("POST inference/provider-connections (create)", () => {
317
271
  ).rejects.toBeInstanceOf(BadRequestError);
318
272
  });
319
273
 
320
- test("rejects openai-compatible creation when the feature flag is disabled", async () => {
321
- setOverridesForTesting({ "openai-compatible-endpoints": false });
322
-
323
- await expect(
324
- call(findHandler("inference_provider_connections_create"), {
325
- body: {
326
- name: "my-compatible",
327
- provider: "openai-compatible",
328
- auth: { type: "api_key", credential: "ref/k" },
329
- base_url: "http://localhost:8080/v1",
330
- models: [{ id: "local-model" }],
331
- },
332
- }),
333
- ).rejects.toBeInstanceOf(BadRequestError);
334
- });
335
-
336
274
  test("throws 400 when auth schema is invalid (api_key without credential)", async () => {
337
275
  await expect(
338
276
  call(findHandler("inference_provider_connections_create"), {
@@ -369,8 +369,12 @@ describe("GET /v1/plugins/search", () => {
369
369
  catalog(ref, [
370
370
  {
371
371
  name: "simple-memory",
372
- path: "experimental/plugins/simple-memory",
373
- source: { kind: "first-party" },
372
+ path: "github:vellum-ai/simple-memory@ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
373
+ source: {
374
+ kind: "github",
375
+ repo: "vellum-ai/simple-memory",
376
+ ref: "ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
377
+ },
374
378
  },
375
379
  {
376
380
  name: "caveman",
@@ -402,8 +406,12 @@ describe("GET /v1/plugins/search", () => {
402
406
  matches: [
403
407
  {
404
408
  name: "simple-memory",
405
- path: "experimental/plugins/simple-memory",
406
- source: { kind: "first-party" },
409
+ path: "github:vellum-ai/simple-memory@ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
410
+ source: {
411
+ kind: "github",
412
+ repo: "vellum-ai/simple-memory",
413
+ ref: "ed09a4c01bf18e4ac8859faee94cb65c7cbd1ca3",
414
+ },
407
415
  },
408
416
  ],
409
417
  });
@@ -458,7 +466,7 @@ describe("GET /v1/plugins/search", () => {
458
466
  test("PluginCatalogUnavailableError → ServiceUnavailableError (503)", async () => {
459
467
  getCatalogSpy.mockImplementation(async () => {
460
468
  throw new PluginCatalogUnavailableError(
461
- "GitHub contents listing failed for experimental/plugins @ main: HTTP 403",
469
+ "GitHub contents listing failed for plugins @ main: HTTP 403",
462
470
  403,
463
471
  );
464
472
  });
@@ -489,8 +497,12 @@ describe("GET /v1/plugins/search", () => {
489
497
  const frozenMatches = Object.freeze([
490
498
  Object.freeze({
491
499
  name: "a",
492
- path: "experimental/plugins/a",
493
- source: { kind: "first-party" } as const,
500
+ path: "github:acme/a@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
501
+ source: {
502
+ kind: "github",
503
+ repo: "acme/a",
504
+ ref: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
505
+ } as const,
494
506
  }),
495
507
  ]) as readonly PluginSearchMatch[];
496
508
  getCatalogSpy.mockImplementation(async (ref) =>
@@ -506,7 +518,11 @@ describe("GET /v1/plugins/search", () => {
506
518
  result.matches.push({
507
519
  name: "b",
508
520
  path: "x",
509
- source: { kind: "first-party" },
521
+ source: {
522
+ kind: "github",
523
+ repo: "acme/b",
524
+ ref: "cccccccccccccccccccccccccccccccccccccccc",
525
+ },
510
526
  }),
511
527
  ).not.toThrow();
512
528
  });
@@ -620,7 +636,11 @@ function pluginDetails(overrides: Partial<PluginDetails> = {}): PluginDetails {
620
636
  homepage: overrides.homepage ?? null,
621
637
  license: overrides.license ?? null,
622
638
  version: overrides.version ?? null,
623
- source: overrides.source ?? { kind: "first-party" },
639
+ source: overrides.source ?? {
640
+ kind: "github",
641
+ repo: "JuliusBrussee/caveman",
642
+ ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
643
+ },
624
644
  readme: overrides.readme ?? null,
625
645
  ref: overrides.ref ?? "main",
626
646
  };
@@ -766,8 +786,8 @@ describe("POST /v1/plugins/install", () => {
766
786
 
767
787
  test("ignores a caller-supplied ref and pins to the curated default", async () => {
768
788
  // Security boundary: installing from an unreviewed ref (a PR branch,
769
- // fork ref, ...) could load attacker-controlled marketplace/first-party
770
- // code, so the HTTP route never honors a body `ref` — it always resolves
789
+ // fork ref, ...) could load attacker-controlled marketplace code, so the
790
+ // HTTP route never honors a body `ref` — it always resolves
771
791
  // against the curated default ref.
772
792
  installSpy.mockImplementation(async (opts) => ({
773
793
  name: opts.name,
@@ -39,6 +39,7 @@ import {
39
39
  import { installAcpConfigStub } from "../../acp/__tests__/helpers/acp-config-stub.js";
40
40
  import { installWhichStub } from "../../acp/__tests__/helpers/which-stub.js";
41
41
  import type { AcpSessionState } from "../../acp/index.js";
42
+ import * as pendingInteractions from "../pending-interactions.js";
42
43
 
43
44
  const config = await installAcpConfigStub();
44
45
  const which = installWhichStub();
@@ -170,6 +171,31 @@ mock.module("../../tools/credentials/broker.js", () => ({
170
171
  },
171
172
  }));
172
173
 
174
+ // Drive the spawn route's high-risk approval gate (ATL-822). `spawnSession`
175
+ // registers a `directResolve` confirmation in `pendingInteractions` and then
176
+ // broadcasts a `confirmation_request`. Real broadcasting needs the event hub /
177
+ // SSE wiring, so the mock auto-resolves the freshly registered interaction
178
+ // the same way `POST /v1/confirm` would (resolve + directResolve). Tests flip
179
+ // `approvalBehavior` to exercise allow / deny, and read `confirmationRequests`
180
+ // to assert the prompt's risk shape. `interaction_resolved` and other event
181
+ // types are ignored.
182
+ type ApprovalBehavior = "allow" | "deny";
183
+ let approvalBehavior: ApprovalBehavior = "allow";
184
+ const confirmationRequests: Array<Record<string, unknown>> = [];
185
+
186
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
187
+ broadcastMessage: (msg: { type?: string; requestId?: string }) => {
188
+ if (msg?.type !== "confirmation_request") return;
189
+ confirmationRequests.push(msg as Record<string, unknown>);
190
+ const decision = approvalBehavior;
191
+ const interaction = pendingInteractions.resolve(
192
+ msg.requestId as string,
193
+ decision === "allow" ? "approved" : "rejected",
194
+ );
195
+ interaction?.directResolve?.(decision);
196
+ },
197
+ }));
198
+
173
199
  import { eq } from "drizzle-orm";
174
200
 
175
201
  import { getDb, getSqlite } from "../../memory/db-connection.js";
@@ -193,6 +219,8 @@ beforeEach(() => {
193
219
  capturedSpawns.length = 0;
194
220
  vaultStore.clear();
195
221
  metadataStore.clear();
222
+ approvalBehavior = "allow";
223
+ confirmationRequests.length = 0;
196
224
  });
197
225
 
198
226
  // ---------------------------------------------------------------------------
@@ -224,6 +252,84 @@ describe("POST /v1/acp/spawn", () => {
224
252
  await expect(handler({ body: { agent: "claude" } })).rejects.toThrow(
225
253
  "agent, task, and conversationId are required",
226
254
  );
255
+ // Guard runs before the approval gate — no prompt is surfaced.
256
+ expect(confirmationRequests).toHaveLength(0);
257
+ });
258
+ });
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // POST /v1/acp/spawn — high-risk approval gate (ATL-822)
262
+ //
263
+ // Spawning an ACP agent is a host subprocess with auto-allowed
264
+ // filesystem/terminal access. The skill-tool path gets the descriptor's
265
+ // `risk: "high"` prompt via ToolExecutor; this route reaches the session
266
+ // manager directly, so it surfaces the same confirmation itself and refuses
267
+ // to spawn unless a guardian approves.
268
+ // ---------------------------------------------------------------------------
269
+
270
+ describe("POST /v1/acp/spawn — approval gate", () => {
271
+ test("surfaces a high-risk host confirmation before spawning", async () => {
272
+ seedVaultToken("test-token-abc123");
273
+
274
+ const handler = getSpawnHandler();
275
+ await handler({
276
+ body: {
277
+ agent: "claude",
278
+ task: "do a thing",
279
+ conversationId: "conv-1",
280
+ cwd: "/work/repo",
281
+ },
282
+ });
283
+
284
+ expect(confirmationRequests).toHaveLength(1);
285
+ const prompt = confirmationRequests[0];
286
+ expect(prompt.toolName).toBe("acp_spawn");
287
+ expect(prompt.riskLevel).toBe("high");
288
+ expect(prompt.executionTarget).toBe("host");
289
+ expect(prompt.conversationId).toBe("conv-1");
290
+ expect((prompt.input as { cwd?: string }).cwd).toBe("/work/repo");
291
+ // Prompt resolved "allow" → spawn proceeds.
292
+ expect(capturedSpawns).toHaveLength(1);
293
+ });
294
+
295
+ test("throws ForbiddenError and does not spawn when the guardian denies", async () => {
296
+ approvalBehavior = "deny";
297
+ seedVaultToken("test-token-abc123");
298
+
299
+ const handler = getSpawnHandler();
300
+ await expect(
301
+ handler({
302
+ body: {
303
+ agent: "claude",
304
+ task: "do a thing",
305
+ conversationId: "conv-1",
306
+ },
307
+ }),
308
+ ).rejects.toThrow(/guardian approval/i);
309
+
310
+ // Denied before any host side effects: no subprocess, no pending leak.
311
+ expect(capturedSpawns).toHaveLength(0);
312
+ expect(pendingInteractions.getAll()).toHaveLength(0);
313
+ });
314
+
315
+ test("denies (and does not spawn) when the client disconnects before approval", async () => {
316
+ seedVaultToken("test-token-abc123");
317
+ const controller = new AbortController();
318
+ controller.abort();
319
+
320
+ const handler = getSpawnHandler();
321
+ await expect(
322
+ handler({
323
+ body: {
324
+ agent: "claude",
325
+ task: "do a thing",
326
+ conversationId: "conv-1",
327
+ },
328
+ abortSignal: controller.signal,
329
+ }),
330
+ ).rejects.toThrow(/guardian approval/i);
331
+
332
+ expect(capturedSpawns).toHaveLength(0);
227
333
  });
228
334
  });
229
335