@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
@@ -100,9 +100,9 @@ const pluginRefCount = new Map<string, number>();
100
100
  /**
101
101
  * Format an owner for log messages and error strings. Returns a stable
102
102
  * human-readable description (e.g. `skill "deploy"`, `plugin "weather"`,
103
- * `MCP server "github"`). When an owner is missing (core tool) or has an
104
- * unrecognized kind, returns a fallback string so log/error sites never
105
- * produce `undefined` interpolations.
103
+ * `MCP server "github"`, `workspace tool override`). When an owner is
104
+ * missing (core tool) or has an unrecognized kind, returns a fallback
105
+ * string so log/error sites never produce `undefined` interpolations.
106
106
  */
107
107
  function describeOwner(owner: OwnerInfo | undefined): string {
108
108
  if (!owner) return "core tool";
@@ -113,11 +113,46 @@ function describeOwner(owner: OwnerInfo | undefined): string {
113
113
  return `plugin "${owner.id}"`;
114
114
  case "mcp":
115
115
  return `MCP server "${owner.id}"`;
116
+ case "workspace":
117
+ return `workspace tool override`;
116
118
  default:
117
119
  return `${(owner as OwnerInfo).kind}-origin tool`;
118
120
  }
119
121
  }
120
122
 
123
+ // ── Workspace tool overrides ─────────────────────────────────────────
124
+ // Two distinct workspace-tool operations both move a core tool entry
125
+ // into this stash:
126
+ //
127
+ // 1. {@link registerWorkspaceTools} — workspace tool with same name as a
128
+ // core tool registers; the original is stashed and the workspace tool
129
+ // takes its place in `tools`. {@link unregisterWorkspaceTool} restores
130
+ // on teardown.
131
+ // 2. {@link removeCoreToolViaWorkspace} — a `<name>.removed` sentinel in
132
+ // `<workspaceDir>/tools/` strips a core tool from the registry. The
133
+ // original is stashed; `tools[name]` is cleared without a replacement.
134
+ // {@link restoreStrippedCoreTool} restores when the sentinel is gone.
135
+ //
136
+ // Both operations are reversible because the stash holds the original
137
+ // core entry verbatim. The two states are distinguished by what (if
138
+ // anything) sits in `tools[name]` and `ownersByName[name]`:
139
+ //
140
+ // - `ownersByName[name].kind === "workspace"` + stash present → override
141
+ // - `tools[name]` absent + stash present → stripped via `.removed`
142
+ // - `tools[name]` present + no owner → normal core tool
143
+ // - `ownersByName[name].kind === "workspace"` + no stash → net-new workspace tool
144
+ //
145
+ // Keyed by tool name. At most one stashed entry per name — the registry
146
+ // rejects a second workspace registration for the same name without an
147
+ // explicit unregister/restore.
148
+ //
149
+ // Plugin/skill/MCP code paths consult this map only indirectly: they see
150
+ // a workspace-kind entry in `ownersByName` (or a stash entry but no live
151
+ // entry) and refuse to register over it, preserving the
152
+ // single-canonical-source invariant the override / strip paths exist to
153
+ // enforce.
154
+ const coreToolOverrides = new Map<string, Tool>();
155
+
121
156
  function withProviderSafeToolName(tool: Tool): Tool {
122
157
  const safeName = toProviderSafeToolName(tool.name);
123
158
  if (safeName === tool.name) {
@@ -185,6 +220,8 @@ export function getToolOwner(name: string): OwnerInfo | undefined {
185
220
  *
186
221
  * Skips any tool whose name collides with a core tool (logs a warning instead
187
222
  * of throwing so the remaining tools in the batch still get registered).
223
+ * Also skips when the name is owned by a workspace tool — workspace
224
+ * overrides are authoritative and silently win over skill registration.
188
225
  * Throws if a tool name collides with a skill tool owned by a different skill.
189
226
  * Allows replacement when the incoming tool has the same skill owner id as
190
227
  * the existing one, which supports hot-reloading a skill without tearing
@@ -198,9 +235,20 @@ export function registerSkillTools(skillId: string, newTools: Tool[]): Tool[] {
198
235
  // Filter out tools that collide with core tools, and validate the rest.
199
236
  const accepted: Tool[] = [];
200
237
  for (const tool of newTools) {
238
+ // A workspace `.removed` sentinel stripped a core tool of this name —
239
+ // the slot is reserved by the stash even though `tools` has no live
240
+ // entry. Refuse to fill the slot from a non-workspace surface.
241
+ if (!tools.has(tool.name) && coreToolOverrides.has(tool.name)) {
242
+ log.warn(
243
+ { toolName: tool.name, skillId },
244
+ `Skill "${skillId}" tried to register tool "${tool.name}" which is reserved by a workspace .removed sentinel. Skipping.`,
245
+ );
246
+ continue;
247
+ }
201
248
  const existing = tools.get(tool.name);
202
249
  if (existing) {
203
- const existingIsCore = !ownersByName.has(tool.name);
250
+ const existingOwner = ownersByName.get(tool.name);
251
+ const existingIsCore = !existingOwner;
204
252
  if (existingIsCore) {
205
253
  log.warn(
206
254
  { toolName: tool.name, ownerSkillId: skillId },
@@ -208,9 +256,15 @@ export function registerSkillTools(skillId: string, newTools: Tool[]): Tool[] {
208
256
  );
209
257
  continue;
210
258
  }
259
+ if (existingOwner?.kind === "workspace") {
260
+ log.warn(
261
+ { toolName: tool.name, skillId },
262
+ `Skill "${skillId}" tried to register tool "${tool.name}" which is owned by a workspace tool override. Skipping.`,
263
+ );
264
+ continue;
265
+ }
211
266
  // Existing is from a different owner (plugin/mcp) or a different
212
267
  // skill — skill tools can only replace themselves (hot-reload).
213
- const existingOwner = ownersByName.get(tool.name);
214
268
  const existingSkillId =
215
269
  existingOwner?.kind === "skill" ? existingOwner.id : undefined;
216
270
  if (existingOwner?.kind !== "skill" || existingSkillId !== skillId) {
@@ -267,6 +321,16 @@ export function registerPluginTools(
267
321
 
268
322
  const accepted: Tool[] = [];
269
323
  for (const tool of stamped) {
324
+ // A workspace `.removed` sentinel stripped a core tool of this name —
325
+ // the slot is reserved by the stash even though `tools` has no live
326
+ // entry. Refuse to fill the slot from a non-workspace surface.
327
+ if (!tools.has(tool.name) && coreToolOverrides.has(tool.name)) {
328
+ log.warn(
329
+ { toolName: tool.name, pluginName },
330
+ `Plugin "${pluginName}" tried to register tool "${tool.name}" which is reserved by a workspace .removed sentinel. Skipping.`,
331
+ );
332
+ continue;
333
+ }
270
334
  const existing = tools.get(tool.name);
271
335
  if (existing) {
272
336
  const existingIsCore = !ownersByName.has(tool.name);
@@ -278,6 +342,13 @@ export function registerPluginTools(
278
342
  continue;
279
343
  }
280
344
  const existingOwner = ownersByName.get(tool.name);
345
+ if (existingOwner?.kind === "workspace") {
346
+ log.warn(
347
+ { toolName: tool.name, pluginName },
348
+ `Plugin "${pluginName}" tried to register tool "${tool.name}" which is owned by a workspace tool override. Skipping.`,
349
+ );
350
+ continue;
351
+ }
281
352
  if (existingOwner?.kind === "plugin") {
282
353
  if (existingOwner.id !== pluginName) {
283
354
  throw new Error(
@@ -382,6 +453,16 @@ export function unregisterSkillTools(skillId: string): void {
382
453
  export function registerMcpTools(serverId: string, newTools: Tool[]): Tool[] {
383
454
  const accepted: Tool[] = [];
384
455
  for (const tool of newTools) {
456
+ // A workspace `.removed` sentinel stripped a core tool of this name —
457
+ // the slot is reserved by the stash even though `tools` has no live
458
+ // entry. Refuse to fill the slot from a non-workspace surface.
459
+ if (!tools.has(tool.name) && coreToolOverrides.has(tool.name)) {
460
+ log.warn(
461
+ { toolName: tool.name, serverId },
462
+ `MCP server "${serverId}" tried to register tool "${tool.name}" which is reserved by a workspace .removed sentinel. Skipping.`,
463
+ );
464
+ continue;
465
+ }
385
466
  const existing = tools.get(tool.name);
386
467
  if (existing) {
387
468
  const existingIsCore = !ownersByName.has(tool.name);
@@ -393,6 +474,13 @@ export function registerMcpTools(serverId: string, newTools: Tool[]): Tool[] {
393
474
  continue;
394
475
  }
395
476
  const existingOwner = ownersByName.get(tool.name);
477
+ if (existingOwner?.kind === "workspace") {
478
+ log.warn(
479
+ { toolName: tool.name, serverId },
480
+ `MCP server "${serverId}" tried to register tool "${tool.name}" which is owned by a workspace tool override. Skipping.`,
481
+ );
482
+ continue;
483
+ }
396
484
  if (existingOwner?.kind === "skill" || existingOwner?.kind === "plugin") {
397
485
  log.warn(
398
486
  {
@@ -458,6 +546,280 @@ export function getSkillToolNames(): string[] {
458
546
  .map((t) => t.name);
459
547
  }
460
548
 
549
+ /**
550
+ * Register a batch of workspace-origin tools — entries discovered under
551
+ * `<workspaceDir>/tools/<name>.{ts,js,json}`. Each call records ownership
552
+ * (`kind: "workspace"`, `id: <workspacePath>`) in `ownersByName` keyed by
553
+ * tool name — the `Tool` object itself carries no owner metadata.
554
+ *
555
+ * Conflict handling:
556
+ *
557
+ * - **Core tool same name**: the original core entry is moved into
558
+ * {@link coreToolOverrides}, and the workspace tool takes its place in
559
+ * `tools`. {@link unregisterWorkspaceTool} restores the original later.
560
+ * - **Workspace tool same name**: rejected with a hard throw. There is
561
+ * exactly one canonical source per workspace tool name on disk; a
562
+ * second registration without an intervening
563
+ * {@link unregisterWorkspaceTool} is a caller bug.
564
+ * - **Plugin / skill / MCP same name**: rejected with a hard throw.
565
+ * Workspace tools must register before any other extension category in
566
+ * the daemon lifecycle (between {@link initializeTools} and
567
+ * {@link loadUserPlugins}); seeing one of these origins here would mean
568
+ * a lifecycle-order regression.
569
+ * - **Net-new name (no existing entry)**: registers as a new tool. The
570
+ * stash stays empty for this name so {@link unregisterWorkspaceTool}
571
+ * simply removes the tool with no restoration.
572
+ *
573
+ * The batch is validated end-to-end before any mutation lands on `tools`
574
+ * or `coreToolOverrides` — a single rejected entry aborts the whole call
575
+ * so callers never observe a partially-applied registration.
576
+ */
577
+ export function registerWorkspaceTools(
578
+ newTools: Array<{
579
+ tool: Tool;
580
+ workspacePath: string;
581
+ }>,
582
+ ): Tool[] {
583
+ // Build provider-safe Tool objects up front. We do not mutate the
584
+ // registry until the entire batch has cleared the conflict checks
585
+ // below. Ownership (kind + workspace path) is tracked separately
586
+ // alongside each stamped entry so the mutation phase can set
587
+ // `ownersByName` in lockstep with `tools`.
588
+ const stamped: Array<{ tool: Tool; workspacePath: string }> = newTools.map(
589
+ ({ tool: workspaceTool, workspacePath }) => ({
590
+ tool: withProviderSafeToolName(workspaceTool as Tool),
591
+ workspacePath,
592
+ }),
593
+ );
594
+
595
+ // Validate the whole batch first so we never leave the registry in a
596
+ // half-applied state. The validation phase only reads; the mutation
597
+ // phase below only fires once every entry has passed.
598
+ const seenInBatch = new Set<string>();
599
+ for (const { tool } of stamped) {
600
+ if (seenInBatch.has(tool.name)) {
601
+ throw new Error(
602
+ `Workspace tool batch contains duplicate name "${tool.name}"`,
603
+ );
604
+ }
605
+ seenInBatch.add(tool.name);
606
+
607
+ const existing = tools.get(tool.name);
608
+ if (!existing) continue;
609
+
610
+ const existingOwner = ownersByName.get(tool.name);
611
+ if (existingOwner?.kind === "workspace") {
612
+ throw new Error(
613
+ `Workspace tool "${tool.name}" is already registered (path: ${existingOwner.id}). Call unregisterWorkspaceTool("${tool.name}") before re-registering.`,
614
+ );
615
+ }
616
+
617
+ if (!existingOwner) continue; // Core tool — override allowed, handled in mutation phase below.
618
+
619
+ throw new Error(
620
+ `Workspace tool "${tool.name}" conflicts with an existing ${describeOwner(existingOwner)}. Workspace tools must register before other extension categories.`,
621
+ );
622
+ }
623
+
624
+ for (const { tool, workspacePath } of stamped) {
625
+ const existing = tools.get(tool.name);
626
+ const existingIsCore = existing && !ownersByName.has(tool.name);
627
+ if (existingIsCore) {
628
+ coreToolOverrides.set(tool.name, existing);
629
+ log.info(
630
+ { name: tool.name, workspacePath },
631
+ "Stashing core tool ahead of workspace override",
632
+ );
633
+ }
634
+ tools.set(tool.name, tool);
635
+ ownersByName.set(tool.name, { kind: "workspace", id: workspacePath });
636
+ log.info(
637
+ {
638
+ name: tool.name,
639
+ workspacePath,
640
+ overridesCore: coreToolOverrides.has(tool.name),
641
+ },
642
+ "Workspace tool registered",
643
+ );
644
+ }
645
+
646
+ return stamped.map(({ tool }) => tool);
647
+ }
648
+
649
+ /**
650
+ * Remove a workspace tool registration. If the name had a stashed core
651
+ * tool ({@link coreToolOverrides}), the original is restored; otherwise
652
+ * the entry is simply deleted (net-new workspace tool case).
653
+ *
654
+ * No-op when the named tool is not currently registered as a workspace
655
+ * tool — the function is safe to call on every shutdown path without
656
+ * needing to track which tools the loader actually registered.
657
+ */
658
+ export function unregisterWorkspaceTool(name: string): void {
659
+ const existingOwner = ownersByName.get(name);
660
+ if (existingOwner?.kind !== "workspace") {
661
+ return;
662
+ }
663
+ const workspacePath = existingOwner.id;
664
+
665
+ const stashed = coreToolOverrides.get(name);
666
+ if (stashed) {
667
+ tools.set(name, stashed);
668
+ ownersByName.delete(name);
669
+ coreToolOverrides.delete(name);
670
+ log.info(
671
+ { name, workspacePath },
672
+ "Workspace tool unregistered — core tool restored",
673
+ );
674
+ return;
675
+ }
676
+
677
+ tools.delete(name);
678
+ ownersByName.delete(name);
679
+ log.info(
680
+ { name, workspacePath },
681
+ "Workspace tool unregistered (no core tool to restore)",
682
+ );
683
+ }
684
+
685
+ /**
686
+ * Strip a core tool from the registry on behalf of a workspace
687
+ * `<name>.removed` sentinel. The original core entry is stashed (same
688
+ * map as override-style stashing) so {@link restoreStrippedCoreTool} can
689
+ * undo the strip if the sentinel file is later removed.
690
+ *
691
+ * No-op cases (logged at debug, never throws):
692
+ * - `name` doesn't exist in the registry — nothing to strip
693
+ * - `name` is already stripped (stash present, live entry absent) — idempotent
694
+ * - `name` is owned by a non-core origin (plugin / skill / mcp) — workspace
695
+ * strip cannot evict another extension's tool; that's a namespacing
696
+ * collision the operator must resolve at the source
697
+ *
698
+ * Throws when `name` is owned by an existing workspace tool. The loader
699
+ * is supposed to filter out files where both `<name>.<ext>` and
700
+ * `<name>.removed` coexist before getting here; if we land in this state
701
+ * something earlier failed and the caller needs to know.
702
+ */
703
+ export function removeCoreToolViaWorkspace(name: string): void {
704
+ const existing = tools.get(name);
705
+
706
+ if (!existing) {
707
+ if (coreToolOverrides.has(name)) {
708
+ log.debug(
709
+ { name },
710
+ "removeCoreToolViaWorkspace: core tool already stripped — no-op",
711
+ );
712
+ return;
713
+ }
714
+ log.debug(
715
+ { name },
716
+ "removeCoreToolViaWorkspace: no tool registered under this name — no-op",
717
+ );
718
+ return;
719
+ }
720
+
721
+ const existingOwner = ownersByName.get(name);
722
+ if (existingOwner?.kind === "workspace") {
723
+ throw new Error(
724
+ `Cannot strip "${name}" via .removed sentinel — name is owned by a workspace tool override (path: ${existingOwner.id}). Remove the workspace tool file first.`,
725
+ );
726
+ }
727
+
728
+ if (existingOwner) {
729
+ log.warn(
730
+ { name, owner: existingOwner },
731
+ `removeCoreToolViaWorkspace: "${name}" is owned by ${describeOwner(existingOwner)}, not a core tool — cannot strip from workspace. Resolve at the source (uninstall the ${existingOwner.kind}).`,
732
+ );
733
+ return;
734
+ }
735
+
736
+ coreToolOverrides.set(name, existing);
737
+ tools.delete(name);
738
+ log.info(
739
+ { name },
740
+ "Stripped core tool via workspace .removed sentinel — stashed for potential restore",
741
+ );
742
+ }
743
+
744
+ /**
745
+ * Restore a core tool that was previously stripped via
746
+ * {@link removeCoreToolViaWorkspace}. Called when the `<name>.removed`
747
+ * sentinel file is deleted (typically by the workspace-tool file watcher).
748
+ *
749
+ * No-op cases (logged at debug, never throws):
750
+ * - No stash exists for `name` — nothing to restore
751
+ * - A workspace tool currently owns the name — the restore is implicit
752
+ * when the workspace tool is later unregistered; doing it here would
753
+ * evict the live workspace tool
754
+ * - A core tool already sits at the name — already restored, idempotent
755
+ */
756
+ export function restoreStrippedCoreTool(name: string): void {
757
+ const stashed = coreToolOverrides.get(name);
758
+ if (!stashed) {
759
+ log.debug(
760
+ { name },
761
+ "restoreStrippedCoreTool: no stashed core tool — no-op",
762
+ );
763
+ return;
764
+ }
765
+ const existing = tools.get(name);
766
+ if (existing) {
767
+ const existingOwner = ownersByName.get(name);
768
+ if (existingOwner?.kind === "workspace") {
769
+ log.debug(
770
+ { name },
771
+ "restoreStrippedCoreTool: workspace tool currently owns this name — leaving stash in place for the workspace tool's eventual unregister",
772
+ );
773
+ return;
774
+ }
775
+ log.debug(
776
+ { name, currentOwner: existingOwner ?? "core" },
777
+ "restoreStrippedCoreTool: a non-workspace entry already sits at this name — leaving stash in place",
778
+ );
779
+ return;
780
+ }
781
+ tools.set(name, stashed);
782
+ coreToolOverrides.delete(name);
783
+ log.info(
784
+ { name },
785
+ "Restored core tool after workspace .removed sentinel was deleted",
786
+ );
787
+ }
788
+
789
+ /**
790
+ * Return the names of all currently registered workspace-origin tools.
791
+ */
792
+ export function getWorkspaceToolNames(): string[] {
793
+ return Array.from(tools.values())
794
+ .filter((t) => ownersByName.get(t.name)?.kind === "workspace")
795
+ .map((t) => t.name);
796
+ }
797
+
798
+ /**
799
+ * Return the names of core tools currently stripped via workspace
800
+ * `.removed` sentinels — i.e. names where the stash holds an entry but
801
+ * no live tool sits in the registry.
802
+ */
803
+ export function getStrippedCoreToolNames(): string[] {
804
+ const stripped: string[] = [];
805
+ for (const name of coreToolOverrides.keys()) {
806
+ if (!tools.has(name)) stripped.push(name);
807
+ }
808
+ return stripped;
809
+ }
810
+
811
+ /**
812
+ * Inspect the override stash for a tool name. Returns the original core
813
+ * tool that was displaced by a workspace registration (or stripped via
814
+ * `.removed`), or `undefined` when no such override exists.
815
+ *
816
+ * Useful for tooling that needs to show "this core tool is overridden by
817
+ * a workspace entry" without exposing the full stash map.
818
+ */
819
+ export function getCoreToolOverride(name: string): Tool | undefined {
820
+ return coreToolOverrides.get(name);
821
+ }
822
+
461
823
  /**
462
824
  * Return the current reference count for a skill's tools. Exposed for testing.
463
825
  */
@@ -566,6 +928,20 @@ export async function initializeTools(): Promise<void> {
566
928
  }
567
929
 
568
930
  log.info({ count: tools.size }, "Tools initialized");
931
+
932
+ // Load workspace tool overrides from `<workspaceDir>/tools/<name>.{ts,js,json}`
933
+ // immediately after core tools have settled, before MCP / plugin
934
+ // registrations get a chance to claim names. This ordering makes
935
+ // workspace tools the canonical owner per name:
936
+ // core registrations → workspace tools → MCP → plugins.
937
+ // Workspace tools land after the core snapshot above so they're never
938
+ // baked into the test-reset baseline.
939
+ //
940
+ // Imported dynamically because the loader imports back from this module
941
+ // (registerWorkspaceTools / removeCoreToolViaWorkspace); a static import
942
+ // here would create a registry ↔ loader cycle.
943
+ const { loadWorkspaceTools } = await import("./workspace-tools/loader.js");
944
+ await loadWorkspaceTools();
569
945
  }
570
946
 
571
947
  /**
@@ -583,6 +959,11 @@ export function __resetRegistryForTesting(): void {
583
959
  ownersByName.clear();
584
960
  skillRefCount.clear();
585
961
  pluginRefCount.clear();
962
+ // Drop the override stash too — the snapshot already represents the
963
+ // pre-override baseline, so leaving stashed entries here would let a
964
+ // later registerWorkspaceTools() falsely report "overridesCore: true"
965
+ // against a fresh registry.
966
+ coreToolOverrides.clear();
586
967
 
587
968
  if (coreToolsSnapshot) {
588
969
  for (const [name, tool] of coreToolsSnapshot) {
@@ -601,6 +982,7 @@ export function __clearRegistryForTesting(): void {
601
982
  ownersByName.clear();
602
983
  skillRefCount.clear();
603
984
  pluginRefCount.clear();
985
+ coreToolOverrides.clear();
604
986
  }
605
987
 
606
988
  /**
@@ -33,6 +33,7 @@ export async function executeScheduleCreate(
33
33
  };
34
34
  }
35
35
  const name = input.name as string;
36
+ const description = input.description as string | undefined;
36
37
  const timezone = (input.timezone as string) ?? null;
37
38
  const message = (input.message as string) ?? "";
38
39
  const script = (input.script as string) ?? null;
@@ -63,6 +64,17 @@ export async function executeScheduleCreate(
63
64
  };
64
65
  }
65
66
 
67
+ if (
68
+ !description ||
69
+ typeof description !== "string" ||
70
+ description.trim().length === 0
71
+ ) {
72
+ return {
73
+ content: "Error: description is required and must be a non-empty string",
74
+ isError: true,
75
+ };
76
+ }
77
+
66
78
  // Validate mode
67
79
  if (!VALID_MODES.includes(mode)) {
68
80
  return {
@@ -128,6 +140,7 @@ export async function executeScheduleCreate(
128
140
  try {
129
141
  const job = createSchedule({
130
142
  name,
143
+ description,
131
144
  cronExpression: null,
132
145
  timezone,
133
146
  message,
@@ -154,6 +167,7 @@ export async function executeScheduleCreate(
154
167
  `One-shot schedule created successfully.`,
155
168
  ` ID: ${job.id}`,
156
169
  ` Name: ${job.name}`,
170
+ ` Description: ${job.description}`,
157
171
  ` Type: one-shot`,
158
172
  ` Mode: ${job.mode}`,
159
173
  ` Fire at: ${fireDate}`,
@@ -211,6 +225,7 @@ export async function executeScheduleCreate(
211
225
  try {
212
226
  const job = createSchedule({
213
227
  name,
228
+ description,
214
229
  cronExpression: resolved.expression,
215
230
  timezone,
216
231
  message,
@@ -243,6 +258,7 @@ export async function executeScheduleCreate(
243
258
  `Recurring schedule created successfully.`,
244
259
  ` ID: ${job.id}`,
245
260
  ` Name: ${job.name}`,
261
+ ` Description: ${job.description}`,
246
262
  ` Syntax: ${job.syntax}`,
247
263
  ` Mode: ${job.mode}`,
248
264
  ` Schedule: ${scheduleDescription}${
@@ -25,6 +25,10 @@ function isOneShot(job: { expression: string | null }): boolean {
25
25
  return job.expression == null;
26
26
  }
27
27
 
28
+ function describeAuthoredPurpose(job: { description: string }): string {
29
+ return job.description || "(none)";
30
+ }
31
+
28
32
  export async function executeScheduleList(
29
33
  input: Record<string, unknown>,
30
34
  _context: ToolContext,
@@ -48,6 +52,7 @@ export async function executeScheduleList(
48
52
  ` Type: ${oneShot ? "one-shot" : "recurring"}`,
49
53
  ` Mode: ${job.mode}`,
50
54
  ` Status: ${job.status}`,
55
+ ` Description: ${describeAuthoredPurpose(job)}`,
51
56
  ];
52
57
 
53
58
  if (oneShot) {
@@ -118,14 +123,17 @@ export async function executeScheduleList(
118
123
  if (oneShot) {
119
124
  const fireTime = formatLocalDate(job.nextRunAt);
120
125
  lines.push(
121
- ` - [${status}] ${job.name} (id: ${job.id}) (one-shot, ${job.mode}) - fire at: ${fireTime} [${job.status}]`,
126
+ ` - [${status}] ${job.name} (id: ${job.id}) (one-shot, ${job.mode}) [${job.status}]`,
127
+ ` Description: ${describeAuthoredPurpose(job)}`,
128
+ ` fire at: ${fireTime}`,
122
129
  );
123
130
  } else {
124
131
  const next = job.enabled ? formatLocalDate(job.nextRunAt) : "n/a";
125
132
  lines.push(
126
- ` - [${status}] ${job.name} (id: ${job.id}) ([${
127
- job.syntax
128
- }] ${describeSchedule(job)}, ${job.mode}) - next: ${next}`,
133
+ ` - [${status}] ${job.name} (id: ${job.id}) (${job.mode})`,
134
+ ` Description: ${describeAuthoredPurpose(job)}`,
135
+ ` Schedule: [${job.syntax}] ${describeSchedule(job)}`,
136
+ ` Next: ${next}`,
129
137
  );
130
138
  }
131
139
  }
@@ -65,6 +65,16 @@ export async function executeScheduleUpdate(
65
65
 
66
66
  const updates: Record<string, unknown> = {};
67
67
  if (input.name !== undefined) updates.name = input.name;
68
+ if (input.description !== undefined) {
69
+ const description = input.description as string;
70
+ if (typeof description !== "string" || description.trim().length === 0) {
71
+ return {
72
+ content: "Error: description must be a non-empty string when provided",
73
+ isError: true,
74
+ };
75
+ }
76
+ updates.description = description;
77
+ }
68
78
  if (input.timezone !== undefined) updates.timezone = input.timezone;
69
79
  if (input.message !== undefined) updates.message = input.message;
70
80
  if (input.script !== undefined) updates.script = input.script;
@@ -184,6 +194,7 @@ export async function executeScheduleUpdate(
184
194
  jobId,
185
195
  updates as {
186
196
  name?: string;
197
+ description?: string;
187
198
  cronExpression?: string;
188
199
  timezone?: string | null;
189
200
  message?: string;
@@ -217,6 +228,7 @@ export async function executeScheduleUpdate(
217
228
  content: [
218
229
  `Schedule updated successfully.`,
219
230
  ` Name: ${job.name}`,
231
+ ` Description: ${job.description}`,
220
232
  ` Syntax: ${job.syntax}`,
221
233
  ` Mode: ${job.mode}`,
222
234
  ` Schedule: ${scheduleDescription}${job.timezone ? ` (${job.timezone})` : ""}`,
@@ -290,11 +290,13 @@ export const skillLoadTool = {
290
290
  skill.inlineCommandExpansions && skill.inlineCommandExpansions.length > 0;
291
291
 
292
292
  if (hasInlineCommands) {
293
- if (skill.source === "extra") {
294
- // Third-party extra roots are out of scope for inline command
295
- // expansion in v1. Reject explicitly so the failure is clear.
293
+ if (skill.source === "extra" || skill.source === "plugin") {
294
+ // Third-party skill roots — `extra` dirs and skills shipped inside
295
+ // installed plugins — are out of scope for inline command expansion.
296
+ // Their bodies are untrusted, so reject explicitly rather than
297
+ // executing shell from them.
296
298
  return {
297
- content: `Error: skill "${skill.id}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
299
+ content: `Error: skill "${skill.id}" contains inline command expansions but inline commands are not supported for third-party (${skill.source}) skill sources.`,
298
300
  isError: true,
299
301
  };
300
302
  }
@@ -367,9 +369,12 @@ export const skillLoadTool = {
367
369
  childLoaded.skill.inlineCommandExpansions.length > 0;
368
370
 
369
371
  if (childHasInlineCommands) {
370
- if (childLoaded.skill.source === "extra") {
372
+ if (
373
+ childLoaded.skill.source === "extra" ||
374
+ childLoaded.skill.source === "plugin"
375
+ ) {
371
376
  return {
372
- content: `Error: included skill "${childId}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
377
+ content: `Error: included skill "${childId}" contains inline command expansions but inline commands are not supported for third-party (${childLoaded.skill.source}) skill sources.`,
373
378
  isError: true,
374
379
  };
375
380
  }
@@ -29,6 +29,8 @@ export const SAFE_ENV_VARS = [
29
29
  "GNUPGHOME",
30
30
  "VELLUM_DEV",
31
31
  "VELLUM_DEBUG",
32
+ "VELLUM_DEVICE_ID",
33
+ "VELLUM_DISABLE_PLATFORM",
32
34
  "VELLUM_ENVIRONMENT",
33
35
 
34
36
  "VELLUM_WORKSPACE_DIR",