@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
@@ -42,11 +42,7 @@ import {
42
42
  registerPlugin,
43
43
  resetPluginRegistryForTests,
44
44
  } from "../plugins/registry.js";
45
- import {
46
- type Plugin,
47
- PluginExecutionError,
48
- type PluginInitContext,
49
- } from "../plugins/types.js";
45
+ import { type Plugin, type PluginInitContext } from "../plugins/types.js";
50
46
  import { APP_VERSION } from "../version.js";
51
47
  import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
52
48
 
@@ -174,9 +170,9 @@ describe("plugin bootstrap", () => {
174
170
  expect(received?.credentials).toEqual({ "some-key": "super-secret-value" });
175
171
  });
176
172
 
177
- test("credential resolution: missing credential fails bootstrap with the plugin named", async () => {
173
+ test("credential resolution: a plugin whose required credential is missing is skipped, not fatal", async () => {
174
+ // GIVEN a plugin that requires a credential the store cannot resolve
178
175
  getSecureKeyAsyncMock.mockImplementation(async () => undefined);
179
-
180
176
  registerPlugin(
181
177
  buildPlugin(
182
178
  "missing-cred",
@@ -185,16 +181,14 @@ describe("plugin bootstrap", () => {
185
181
  ),
186
182
  );
187
183
 
188
- let caught: unknown;
189
- try {
190
- await bootstrapPlugins();
191
- } catch (err) {
192
- caught = err;
193
- }
194
- expect(caught).toBeInstanceOf(PluginExecutionError);
195
- const msg = (caught as PluginExecutionError).message;
196
- expect(msg).toContain("missing-cred");
197
- expect(msg).toContain("absent-key");
184
+ // WHEN bootstrap runs
185
+ // THEN it completes without throwing — the unresolvable credential is
186
+ // contained to that plugin (same per-plugin isolation as an init throw)
187
+ await bootstrapPlugins();
188
+
189
+ // AND the plugin is dropped from the registry
190
+ const names = getRegisteredPlugins().map((p) => p.manifest.name);
191
+ expect(names).not.toContain("missing-cred");
198
192
  });
199
193
 
200
194
  test("version mismatch: external plugin loader rejects when peerDependency unsatisfied", async () => {
@@ -208,7 +202,8 @@ describe("plugin bootstrap", () => {
208
202
  expect(() => registerPlugin(plugin)).not.toThrow();
209
203
  });
210
204
 
211
- test("plugin init throw: bootstrap throws a PluginExecutionError naming the plugin", async () => {
205
+ test("plugin init throw: bootstrap contains the failure and does not throw", async () => {
206
+ // GIVEN a plugin whose init throws
212
207
  registerPlugin(
213
208
  buildPlugin("broken", {
214
209
  async init() {
@@ -217,65 +212,91 @@ describe("plugin bootstrap", () => {
217
212
  }),
218
213
  );
219
214
 
220
- let caught: unknown;
221
- try {
222
- await bootstrapPlugins();
223
- } catch (err) {
224
- caught = err;
225
- }
226
- expect(caught).toBeInstanceOf(PluginExecutionError);
227
- const msg = (caught as PluginExecutionError).message;
228
- expect(msg).toContain("broken");
229
- expect(msg).toContain("kaboom");
215
+ // WHEN bootstrap runs
216
+ // THEN it completes without throwing — a single plugin's init failure is
217
+ // contained to that plugin rather than aborting the whole plugin layer
218
+ await bootstrapPlugins();
219
+
220
+ // AND the failing plugin is dropped from the registry so its hooks never
221
+ // participate in the turn lifecycle
222
+ const names = getRegisteredPlugins().map((p) => p.manifest.name);
223
+ expect(names).not.toContain("broken");
230
224
  });
231
225
 
232
- test("partial-init failure: earlier plugins' onShutdown runs in reverse before the error propagates", async () => {
233
- // If plugin N throws during init, every plugin 1..N-1 that already made
234
- // it through its full init+contribution phase must have onShutdown()
235
- // invoked in reverse registration order before bootstrap re-throws.
236
- // Without this, earlier plugins leak live tools/routes/skills because
237
- // the shutdown hook is only registered once the entire loop completes.
238
- const callOrder: string[] = [];
226
+ test("mid-list init failure: surrounding plugins still initialize and survive", async () => {
227
+ // GIVEN two healthy plugins surrounding one whose init throws, registered
228
+ // in order
229
+ const initialized: string[] = [];
230
+ const shutDownDuringBootstrap: string[] = [];
239
231
  registerPlugin(
240
- buildPlugin("survivor-a", {
241
- async init() {},
232
+ buildPlugin("before", {
233
+ async init() {
234
+ initialized.push("before");
235
+ },
242
236
  async onShutdown() {
243
- callOrder.push("survivor-a");
237
+ shutDownDuringBootstrap.push("before");
244
238
  },
245
239
  }),
246
240
  );
247
241
  registerPlugin(
248
- buildPlugin("survivor-b", {
249
- async init() {},
250
- async onShutdown() {
251
- callOrder.push("survivor-b");
242
+ buildPlugin("failing", {
243
+ async init() {
244
+ throw new Error("mid-bootstrap failure");
252
245
  },
253
246
  }),
254
247
  );
255
248
  registerPlugin(
256
- buildPlugin("failing", {
249
+ buildPlugin("after", {
257
250
  async init() {
258
- throw new Error("mid-bootstrap failure");
251
+ initialized.push("after");
259
252
  },
260
253
  async onShutdown() {
261
- // Never called — this plugin never completes init, so it was never
262
- // added to the active list that teardown walks.
263
- callOrder.push("failing");
254
+ shutDownDuringBootstrap.push("after");
264
255
  },
265
256
  }),
266
257
  );
267
258
 
268
- let caught: unknown;
269
- try {
270
- await bootstrapPlugins();
271
- } catch (err) {
272
- caught = err;
273
- }
274
- expect(caught).toBeInstanceOf(PluginExecutionError);
275
-
276
- // Reverse order: survivor-b registered after survivor-a, so it tears
277
- // down first; "failing" never entered the active list.
278
- expect(callOrder).toEqual(["survivor-b", "survivor-a"]);
259
+ // WHEN bootstrap runs
260
+ await bootstrapPlugins();
261
+
262
+ // THEN both healthy plugins initialized — the one registered after the
263
+ // failure is not skipped
264
+ expect(initialized).toEqual(["before", "after"]);
265
+ // AND neither healthy plugin was torn down by the failure (their
266
+ // onShutdown only runs at real shutdown, not during bootstrap)
267
+ expect(shutDownDuringBootstrap).toEqual([]);
268
+ // AND the failing plugin is dropped while both survivors remain registered
269
+ const names = getRegisteredPlugins().map((p) => p.manifest.name);
270
+ expect(names).toContain("before");
271
+ expect(names).toContain("after");
272
+ expect(names).not.toContain("failing");
273
+ });
274
+
275
+ test("user-plugin init failure leaves the first-party defaults registered", async () => {
276
+ // GIVEN the first-party defaults are registered ahead of user plugins, as
277
+ // at daemon startup
278
+ registerDefaultPlugins();
279
+ // AND a user plugin registered after them whose init throws
280
+ registerPlugin(
281
+ buildPlugin("breaking-user-plugin", {
282
+ async init() {
283
+ throw new Error("user plugin boom");
284
+ },
285
+ }),
286
+ );
287
+
288
+ // WHEN bootstrap runs
289
+ await bootstrapPlugins();
290
+
291
+ // THEN the failing user plugin is dropped from the registry
292
+ const names = getRegisteredPlugins().map((p) => p.manifest.name);
293
+ expect(names).not.toContain("breaking-user-plugin");
294
+ // AND every first-party default survived, so core turn behavior (memory
295
+ // retrieval, history repair, title generation) keeps running in degraded
296
+ // mode instead of being torn down with the failing plugin
297
+ expect(names).toContain("default-memory-retrieval");
298
+ expect(names).toContain("default-history-repair");
299
+ expect(names).toContain("default-title-generate");
279
300
  });
280
301
 
281
302
  test("shutdown order: onShutdown fires in reverse registration order", async () => {
@@ -378,11 +399,11 @@ describe("plugin bootstrap", () => {
378
399
  expect(initFired).toBe(true);
379
400
  });
380
401
 
381
- test("requiresFlag disabled: init does not fire and no tools/routes/skills are registered", async () => {
402
+ test("requiresFlag disabled: init does not fire and no tools/routes are registered", async () => {
382
403
  setOverridesForTesting({ "plugin-gated-disabled": false });
383
404
 
384
405
  let initFired = false;
385
- // Attach tool/route/skill contributions alongside init. If gating works,
406
+ // Attach tool/route contributions alongside init. If gating works,
386
407
  // none of them should land in their respective registries.
387
408
  const plugin = buildPlugin(
388
409
  "gated-off",
@@ -409,24 +430,14 @@ describe("plugin bootstrap", () => {
409
430
  handler: async () => new Response("ok"),
410
431
  },
411
432
  ],
412
- skills: [
413
- {
414
- id: "gated-off/skill",
415
- name: "gated-off-skill",
416
- description: "should not be catalogued",
417
- body: "# unused",
418
- },
419
- ],
420
433
  },
421
434
  { requiresFlag: ["plugin-gated-disabled"] },
422
435
  );
423
436
  registerPlugin(plugin);
424
437
 
425
- // Grab tool / route / skill introspection helpers lazily so the import
438
+ // Grab tool / route introspection helpers lazily so the import
426
439
  // side effect happens after `mock.module` has taken effect.
427
440
  const { getTool } = await import("../tools/registry.js");
428
- const { getPluginSkillRefCount } =
429
- await import("../plugins/plugin-skill-contributions.js");
430
441
  const { matchSkillRoute } =
431
442
  await import("../runtime/skill-route-registry.js");
432
443
 
@@ -438,8 +449,6 @@ describe("plugin bootstrap", () => {
438
449
  expect(getTool("gated-off-tool")).toBeUndefined();
439
450
  // No route wired up — `matchSkillRoute` returns null when nothing matches.
440
451
  expect(matchSkillRoute("/_plugin/gated-off/status", "GET")).toBeNull();
441
- // No skill catalogued under this plugin's name — ref count stays 0.
442
- expect(getPluginSkillRefCount("gated-off")).toBe(0);
443
452
  });
444
453
 
445
454
  test("requiresFlag absent: plugin activates unconditionally", async () => {
@@ -218,9 +218,7 @@ describe("plugin tool contributions", () => {
218
218
  });
219
219
 
220
220
  test("tools are only registered after init() succeeds", async () => {
221
- // A plugin whose init throws must not contribute tools — the bootstrap
222
- // aborts with a PluginExecutionError, and nothing from this plugin
223
- // should leak into the tool registry.
221
+ // GIVEN a plugin that declares a tool but throws during init()
224
222
  const plugin = buildPlugin("delta-broken", {
225
223
  async init() {
226
224
  throw new Error("boom");
@@ -229,7 +227,12 @@ describe("plugin tool contributions", () => {
229
227
  });
230
228
  registerPlugin(plugin);
231
229
 
232
- await expect(bootstrapPlugins()).rejects.toThrow(/delta-broken/);
230
+ // WHEN bootstrap runs
231
+ // THEN it does not throw — the init failure is contained to this plugin
232
+ await bootstrapPlugins();
233
+
234
+ // AND the failing plugin's tool is rolled back, never leaking into the
235
+ // registry
233
236
  expect(getTool("delta-tool")).toBeUndefined();
234
237
  });
235
238
  });
@@ -78,14 +78,6 @@ describe("plugin core types", () => {
78
78
  handler: async () => new Response("ok", { status: 200 }),
79
79
  },
80
80
  ],
81
- skills: [
82
- {
83
- id: "sample-skill",
84
- name: "Sample Skill",
85
- description: "Demo plugin-contributed skill",
86
- body: "## Sample\n\nPlugin-provided skill body.",
87
- },
88
- ],
89
81
  } satisfies Plugin;
90
82
 
91
83
  // Minimal runtime check so the test body is non-empty.
@@ -19,19 +19,11 @@ function makeConfig(): AssistantConfig {
19
19
  }
20
20
 
21
21
  describe("getVisibleProviderCatalog", () => {
22
- test("hides openai-compatible endpoints by default", () => {
22
+ test("shows openai-compatible endpoints unconditionally (GA'ed)", () => {
23
23
  setOverridesForTesting({});
24
24
 
25
25
  const visible = getVisibleProviderCatalog(makeConfig());
26
26
 
27
- expect(visible.find((p) => p.id === "openai-compatible")).toBeUndefined();
28
- });
29
-
30
- test("shows openai-compatible endpoints when its flag is enabled", () => {
31
- setOverridesForTesting({ "openai-compatible-endpoints": true });
32
-
33
- const visible = getVisibleProviderCatalog(makeConfig());
34
-
35
27
  expect(visible.find((p) => p.id === "openai-compatible")).toBeDefined();
36
28
  });
37
29
 
@@ -0,0 +1,99 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger.
4
+ mock.module("../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ }));
10
+
11
+ import type { AssistantConfig } from "../config/schema.js";
12
+ import { getDb } from "../memory/db-connection.js";
13
+ import { initializeDb } from "../memory/db-init.js";
14
+ import { pruneOldConversationsJob } from "../memory/job-handlers/cleanup.js";
15
+ import type { MemoryJob } from "../memory/jobs-store.js";
16
+ import {
17
+ conversations,
18
+ skillLoadedEvents,
19
+ toolInvocations,
20
+ } from "../memory/schema.js";
21
+
22
+ initializeDb();
23
+
24
+ const STALE_ID = "conv-prune-job-stale";
25
+ const FRESH_ID = "conv-prune-job-fresh";
26
+
27
+ const JOB = { payload: {} } as unknown as MemoryJob;
28
+ const CONFIG = {
29
+ memory: { cleanup: { conversationRetentionDays: 30 } },
30
+ } as unknown as AssistantConfig;
31
+
32
+ function seedConversation(id: string, updatedAt: number): void {
33
+ const db = getDb();
34
+ db.insert(conversations)
35
+ .values({ id, title: "test", createdAt: updatedAt, updatedAt })
36
+ .run();
37
+ db.insert(toolInvocations)
38
+ .values({
39
+ id: `ti-${id}`,
40
+ conversationId: id,
41
+ toolName: "calendar_list_events",
42
+ input: "{}",
43
+ result: "{}",
44
+ decision: "allow",
45
+ riskLevel: "low",
46
+ durationMs: 12,
47
+ createdAt: updatedAt,
48
+ })
49
+ .run();
50
+ db.insert(skillLoadedEvents)
51
+ .values({
52
+ id: `sl-${id}`,
53
+ createdAt: updatedAt,
54
+ conversationId: id,
55
+ skillName: "test-skill",
56
+ })
57
+ .run();
58
+ }
59
+
60
+ function countRows(conversationId: string): {
61
+ invocations: number;
62
+ skillLoads: number;
63
+ } {
64
+ const db = getDb();
65
+ return {
66
+ invocations: db
67
+ .select()
68
+ .from(toolInvocations)
69
+ .all()
70
+ .filter((r) => r.conversationId === conversationId).length,
71
+ skillLoads: db
72
+ .select()
73
+ .from(skillLoadedEvents)
74
+ .all()
75
+ .filter((r) => r.conversationId === conversationId).length,
76
+ };
77
+ }
78
+
79
+ describe("pruneOldConversationsJob", () => {
80
+ beforeEach(() => {
81
+ const db = getDb();
82
+ db.delete(skillLoadedEvents).run();
83
+ db.delete(toolInvocations).run();
84
+ db.delete(conversations).run();
85
+ });
86
+
87
+ test("deletes non-cascading rows — including skill_loaded_events — for pruned conversations only", () => {
88
+ const staleUpdatedAt = Date.now() - 60 * 86_400_000;
89
+ seedConversation(STALE_ID, staleUpdatedAt);
90
+ seedConversation(FRESH_ID, Date.now());
91
+
92
+ pruneOldConversationsJob(JOB, CONFIG);
93
+
94
+ expect(countRows(STALE_ID)).toEqual({ invocations: 0, skillLoads: 0 });
95
+ expect(countRows(FRESH_ID)).toEqual({ invocations: 1, skillLoads: 1 });
96
+ const remaining = getDb().select().from(conversations).all();
97
+ expect(remaining.map((c) => c.id)).toEqual([FRESH_ID]);
98
+ });
99
+ });
@@ -6,17 +6,26 @@ import {
6
6
  __resetRegistryForTesting,
7
7
  getAllToolDefinitions,
8
8
  getAllTools,
9
+ getCoreToolOverride,
9
10
  getSkillRefCount,
10
11
  getSkillToolNames,
11
12
  getTool,
12
13
  getToolOwner,
14
+ getWorkspaceToolNames,
13
15
  initializeTools,
16
+ registerPluginTools,
14
17
  registerSkillTools,
15
18
  registerTool,
19
+ registerWorkspaceTools,
16
20
  unregisterSkillTools,
21
+ unregisterWorkspaceTool,
17
22
  } from "../tools/registry.js";
18
23
  import { eagerModuleToolNames, explicitTools } from "../tools/tool-manifest.js";
19
- import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
24
+ import type {
25
+ Tool,
26
+ ToolContext,
27
+ ToolExecutionResult,
28
+ } from "../tools/types.js";
20
29
 
21
30
  // Clean up global registry after this file completes to prevent
22
31
  // contamination of subsequent test files in combined runs.
@@ -414,3 +423,233 @@ describe("skill tool reference counting", () => {
414
423
  expect(getSkillRefCount("nonexistent-skill")).toBe(0);
415
424
  });
416
425
  });
426
+
427
+ // ─────────────────────────────────────────────────────────────────────
428
+ // Workspace tool overrides
429
+ //
430
+ // Tests the registry surface exposed by registerWorkspaceTools /
431
+ // unregisterWorkspaceTool / getCoreToolOverride / getWorkspaceToolNames.
432
+ // The loader (workspace-tools/loader.ts) is tested separately — these
433
+ // tests treat the registry contract as the source of truth and exercise
434
+ // it directly with synthetic tool objects.
435
+ // ─────────────────────────────────────────────────────────────────────
436
+
437
+ function makeWorkspaceInput(
438
+ name: string,
439
+ workspacePath = `/workspace/tools/${name}.ts`,
440
+ ): { tool: Tool; workspacePath: string } {
441
+ return {
442
+ workspacePath,
443
+ tool: {
444
+ name,
445
+ category: "workspace",
446
+ description: `Workspace ${name}`,
447
+ defaultRiskLevel: RiskLevel.Low,
448
+ executionTarget: "sandbox",
449
+ input_schema: { type: "object", properties: {}, required: [] },
450
+ async execute(
451
+ _input: Record<string, unknown>,
452
+ _context: ToolContext,
453
+ ): Promise<ToolExecutionResult> {
454
+ return { content: `workspace ${name}`, isError: false };
455
+ },
456
+ },
457
+ };
458
+ }
459
+
460
+ function makeLoadedPluginTool(name: string): Tool {
461
+ return {
462
+ name,
463
+ category: "plugin",
464
+ description: `Plugin ${name}`,
465
+ defaultRiskLevel: RiskLevel.Low,
466
+ executionTarget: "sandbox",
467
+ input_schema: { type: "object", properties: {}, required: [] },
468
+ async execute(
469
+ _input: Record<string, unknown>,
470
+ _context: ToolContext,
471
+ ): Promise<ToolExecutionResult> {
472
+ return { content: `plugin ${name}`, isError: false };
473
+ },
474
+ };
475
+ }
476
+
477
+ describe("workspace tool registry", () => {
478
+ beforeEach(() => {
479
+ __resetRegistryForTesting();
480
+ });
481
+
482
+ test("net-new workspace tool registers without stashing anything", () => {
483
+ __clearRegistryForTesting();
484
+
485
+ const accepted = registerWorkspaceTools([makeWorkspaceInput("new_tool")]);
486
+
487
+ expect(accepted).toHaveLength(1);
488
+ const tool = getTool("new_tool");
489
+ expect(tool).toBeDefined();
490
+ expect(getToolOwner("new_tool")).toEqual({
491
+ kind: "workspace",
492
+ id: "/workspace/tools/new_tool.ts",
493
+ });
494
+ expect(getCoreToolOverride("new_tool")).toBeUndefined();
495
+ expect(getWorkspaceToolNames()).toEqual(["new_tool"]);
496
+ });
497
+
498
+ test("workspace tool with same name as core stashes the original", () => {
499
+ __clearRegistryForTesting();
500
+ const coreTool = makeFakeTool("override_target");
501
+ registerTool(coreTool);
502
+ expect(getTool("override_target")).toEqual(coreTool);
503
+
504
+ registerWorkspaceTools([makeWorkspaceInput("override_target")]);
505
+
506
+ // Live registry now points at the workspace tool.
507
+ const live = getTool("override_target");
508
+ expect(live).toBeDefined();
509
+ expect(getToolOwner("override_target")?.kind).toBe("workspace");
510
+ expect(getToolOwner("override_target")?.kind).toBe("workspace");
511
+
512
+ // Stash preserves the original for later restoration.
513
+ expect(getCoreToolOverride("override_target")).toEqual(coreTool);
514
+ });
515
+
516
+ test("unregister restores the stashed core tool when one exists", () => {
517
+ __clearRegistryForTesting();
518
+ const coreTool = makeFakeTool("restore_me");
519
+ registerTool(coreTool);
520
+ registerWorkspaceTools([makeWorkspaceInput("restore_me")]);
521
+ expect(getToolOwner("restore_me")?.kind).toBe("workspace");
522
+
523
+ unregisterWorkspaceTool("restore_me");
524
+
525
+ expect(getTool("restore_me")).toEqual(coreTool);
526
+ expect(getCoreToolOverride("restore_me")).toBeUndefined();
527
+ expect(getWorkspaceToolNames()).toEqual([]);
528
+ });
529
+
530
+ test("unregister deletes the tool when no core counterpart was stashed", () => {
531
+ __clearRegistryForTesting();
532
+ registerWorkspaceTools([makeWorkspaceInput("only_workspace")]);
533
+ expect(getTool("only_workspace")).toBeDefined();
534
+
535
+ unregisterWorkspaceTool("only_workspace");
536
+
537
+ expect(getTool("only_workspace")).toBeUndefined();
538
+ expect(getCoreToolOverride("only_workspace")).toBeUndefined();
539
+ });
540
+
541
+ test("unregister is a no-op on a non-workspace name", () => {
542
+ __clearRegistryForTesting();
543
+ const coreTool = makeFakeTool("not_workspace");
544
+ registerTool(coreTool);
545
+
546
+ unregisterWorkspaceTool("not_workspace");
547
+ unregisterWorkspaceTool("never_existed");
548
+
549
+ expect(getTool("not_workspace")).toEqual(coreTool);
550
+ });
551
+
552
+ test("re-registering an existing workspace tool throws (single canonical source)", () => {
553
+ __clearRegistryForTesting();
554
+ registerWorkspaceTools([
555
+ makeWorkspaceInput("dupe", "/workspace/tools/dupe"),
556
+ ]);
557
+
558
+ expect(() =>
559
+ registerWorkspaceTools([
560
+ makeWorkspaceInput("dupe", "/workspace/tools/dupe"),
561
+ ]),
562
+ ).toThrow(/already registered/);
563
+ });
564
+
565
+ test("duplicate names within a single batch throw (no partial application)", () => {
566
+ __clearRegistryForTesting();
567
+
568
+ expect(() =>
569
+ registerWorkspaceTools([
570
+ makeWorkspaceInput("twin"),
571
+ makeWorkspaceInput("twin", "/workspace/tools/twin-alt"),
572
+ ]),
573
+ ).toThrow(/duplicate name/);
574
+
575
+ // Mutation phase never ran — registry stays empty for this name.
576
+ expect(getTool("twin")).toBeUndefined();
577
+ expect(getWorkspaceToolNames()).toEqual([]);
578
+ });
579
+
580
+ test("batch rejection leaves earlier-batch tools un-mutated", () => {
581
+ __clearRegistryForTesting();
582
+
583
+ // Pre-occupy "guarded" as a skill tool — workspace tools cannot
584
+ // override skill tools (only core), so the batch must throw and
585
+ // leave the otherwise-fine "ok_a" / "ok_b" entries unregistered.
586
+ registerSkillTools("skill-a", [makeSkillTool("guarded")]);
587
+
588
+ expect(() =>
589
+ registerWorkspaceTools([
590
+ makeWorkspaceInput("ok_a"),
591
+ makeWorkspaceInput("guarded"),
592
+ makeWorkspaceInput("ok_b"),
593
+ ]),
594
+ ).toThrow(/conflicts with an existing/);
595
+
596
+ expect(getTool("ok_a")).toBeUndefined();
597
+ expect(getTool("ok_b")).toBeUndefined();
598
+ // The skill tool is still in the registry, untouched.
599
+ expect(getToolOwner("guarded")?.kind).toBe("skill");
600
+ });
601
+
602
+ test("workspace tool blocks subsequent plugin registration with same name", () => {
603
+ __clearRegistryForTesting();
604
+ registerWorkspaceTools([makeWorkspaceInput("contested")]);
605
+
606
+ // Plugin attempts to register over a workspace-owned name. The
607
+ // registry's plugin-registration path warn-skips rather than
608
+ // throwing, mirroring the core-conflict behavior.
609
+ const accepted = registerPluginTools("some-plugin", [
610
+ makeLoadedPluginTool("contested"),
611
+ ]);
612
+
613
+ expect(accepted).toEqual([]);
614
+ expect(getToolOwner("contested")?.kind).toBe("workspace");
615
+ });
616
+
617
+ test("workspace tool blocks subsequent skill registration with same name", () => {
618
+ __clearRegistryForTesting();
619
+ registerWorkspaceTools([makeWorkspaceInput("skill_attempt")]);
620
+
621
+ const accepted = registerSkillTools("some-skill", [
622
+ makeSkillTool("skill_attempt"),
623
+ ]);
624
+
625
+ expect(accepted).toEqual([]);
626
+ expect(getToolOwner("skill_attempt")?.kind).toBe("workspace");
627
+ expect(getSkillToolNames()).toEqual([]);
628
+ });
629
+
630
+ test("__resetRegistryForTesting clears the override stash", () => {
631
+ __clearRegistryForTesting();
632
+ const coreTool = makeFakeTool("baseline_tool");
633
+ registerTool(coreTool);
634
+ registerWorkspaceTools([makeWorkspaceInput("baseline_tool")]);
635
+ expect(getCoreToolOverride("baseline_tool")).toEqual(coreTool);
636
+
637
+ __resetRegistryForTesting();
638
+
639
+ expect(getCoreToolOverride("baseline_tool")).toBeUndefined();
640
+ });
641
+
642
+ test("workspace tool overrides surface as the live tool in getAllToolDefinitions", () => {
643
+ __clearRegistryForTesting();
644
+ const coreTool = makeFakeTool("listed_tool");
645
+ registerTool(coreTool);
646
+ registerWorkspaceTools([makeWorkspaceInput("listed_tool")]);
647
+
648
+ const definitions = getAllToolDefinitions();
649
+ const ours = definitions.find((d) => d.name === "listed_tool");
650
+ expect(ours).toBeDefined();
651
+ // The live workspace tool defines its own description; ensure the
652
+ // definition does not leak the core tool's description.
653
+ expect(ours?.description).toBe("Workspace listed_tool");
654
+ });
655
+ });
@@ -125,6 +125,9 @@ mock.module("../tools/registry.js", () => ({
125
125
 
126
126
  mock.module("../permissions/gateway-threshold-reader.js", () => ({
127
127
  getAutoApproveThreshold: async () => "medium",
128
+ // Refresh failure ("null") keeps the original decision — these tests
129
+ // exercise the cached-threshold paths only.
130
+ refreshAutoApproveThreshold: async () => null,
128
131
  _clearGlobalCacheForTesting: () => {},
129
132
  }));
130
133