@vellumai/assistant 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -1,13 +1,15 @@
1
1
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
2
+ import { getIsContainerized } from "../config/env-registry.js";
2
3
  import { getConfig } from "../config/loader.js";
3
- import { getHookManager } from "../hooks/manager.js";
4
4
  import { resolveThreshold } from "../permissions/approval-policy.js";
5
5
  import {
6
6
  check,
7
7
  classifyRisk,
8
8
  generateAllowlistOptions,
9
9
  generateScopeOptions,
10
+ getCachedAssessment,
10
11
  } from "../permissions/checker.js";
12
+ import { getAutoApproveThreshold } from "../permissions/gateway-threshold-reader.js";
11
13
  import type { PermissionPrompter } from "../permissions/prompter.js";
12
14
  import { addRule } from "../permissions/trust-store.js";
13
15
  import { RiskLevel } from "../permissions/types.js";
@@ -35,8 +37,27 @@ export type PermissionDecision =
35
37
  decision: string;
36
38
  riskLevel: string;
37
39
  wasPrompted?: boolean;
40
+ /** Risk metadata from the classifier assessment cache (when available). */
41
+ riskMeta?: {
42
+ riskLevel: string;
43
+ riskReason: string;
44
+ riskScopeOptions: Array<{ pattern: string; label: string }>;
45
+ isContainerized?: boolean;
46
+ };
38
47
  }
39
- | { allowed: false; decision: string; riskLevel: string; content: string };
48
+ | {
49
+ allowed: false;
50
+ decision: string;
51
+ riskLevel: string;
52
+ content: string;
53
+ /** Risk metadata from the classifier assessment cache (when available). */
54
+ riskMeta?: {
55
+ riskLevel: string;
56
+ riskReason: string;
57
+ riskScopeOptions: Array<{ pattern: string; label: string }>;
58
+ isContainerized?: boolean;
59
+ };
60
+ };
40
61
 
41
62
  export class PermissionChecker {
42
63
  private prompter: PermissionPrompter;
@@ -58,10 +79,6 @@ export class PermissionChecker {
58
79
  context: ToolContext,
59
80
  executionTarget: ExecutionTarget,
60
81
  emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
61
- sanitizeToolInput: (
62
- toolName: string,
63
- input: Record<string, unknown>,
64
- ) => Record<string, unknown>,
65
82
  startTime: number,
66
83
  computePreviewDiff: (
67
84
  toolName: string,
@@ -111,6 +128,19 @@ export class PermissionChecker {
111
128
  );
112
129
  const riskLevel: string = risk;
113
130
 
131
+ // Look up the cached assessment to extract risk metadata for the tool result.
132
+ // This is populated by classifyRisk() for classifier-backed tools (bash, file, web, skill).
133
+ // For tools without classifiers (e.g. MCP tools), the cache returns undefined.
134
+ const cachedAssessment = getCachedAssessment(name, input);
135
+ const riskMeta = cachedAssessment
136
+ ? {
137
+ riskLevel: cachedAssessment.riskLevel,
138
+ riskReason: cachedAssessment.reason,
139
+ riskScopeOptions: cachedAssessment.scopeOptions,
140
+ isContainerized: getIsContainerized(),
141
+ }
142
+ : undefined;
143
+
114
144
  // Wrap the rest of permission evaluation so that any exception
115
145
  // carries the classified risk level back to the caller. Without
116
146
  // this, the executor's catch block would fall back to the default
@@ -179,6 +209,7 @@ export class PermissionChecker {
179
209
  decision: "denied",
180
210
  riskLevel,
181
211
  content: result.reason,
212
+ riskMeta,
182
213
  };
183
214
  }
184
215
 
@@ -199,7 +230,12 @@ export class PermissionChecker {
199
230
  { toolName: name, riskLevel },
200
231
  "Auto-approving bash tool for platform-hosted guardian session",
201
232
  );
202
- return { allowed: true, decision: "platform_auto_approve", riskLevel };
233
+ return {
234
+ allowed: true,
235
+ decision: "platform_auto_approve",
236
+ riskLevel,
237
+ riskMeta,
238
+ };
203
239
  }
204
240
 
205
241
  if (result.decision === "prompt") {
@@ -218,39 +254,50 @@ export class PermissionChecker {
218
254
  const isDynamicSkillLoad =
219
255
  result.matchedRule?.pattern.startsWith("skill_load_dynamic:") ===
220
256
  true;
221
- const bgThreshold = resolveThreshold(
222
- cfg.permissions.autoApproveUpTo,
223
- "background",
224
- );
225
- const thresholdOrdinal: Record<string, number> = {
226
- none: -1,
227
- low: 0,
228
- medium: 1,
229
- };
230
- const riskOrdinal: Record<string, number> = {
231
- [RiskLevel.Low]: 0,
232
- [RiskLevel.Medium]: 1,
233
- [RiskLevel.High]: 2,
234
- };
235
- const withinThreshold =
236
- (riskOrdinal[riskLevel] ?? 2) <= (thresholdOrdinal[bgThreshold] ?? 0);
237
257
  if (
238
258
  context.isInteractive === false &&
239
259
  context.trustClass === "guardian" &&
240
260
  !context.requireFreshApproval &&
241
261
  !isDynamicSkillLoad &&
242
- !v2ForcePrompt &&
243
- withinThreshold
262
+ !v2ForcePrompt
244
263
  ) {
245
- log.info(
246
- { toolName: name, riskLevel },
247
- "Auto-approving for non-interactive guardian session",
264
+ // Use gateway threshold when v3 is enabled, falling back to config.
265
+ // getAutoApproveThreshold returns from cache (populated by check() above).
266
+ // Deferred inside the non-interactive branch so interactive prompts
267
+ // don't pay the gateway I/O cost.
268
+ const gatewayBgThreshold = await getAutoApproveThreshold(
269
+ context.conversationId,
270
+ "background",
248
271
  );
249
- return {
250
- allowed: true,
251
- decision: "guardian_auto_approve",
252
- riskLevel,
272
+ const bgThreshold =
273
+ gatewayBgThreshold ??
274
+ resolveThreshold(cfg.permissions.autoApproveUpTo, "background");
275
+ const thresholdOrdinal: Record<string, number> = {
276
+ none: -1,
277
+ low: 0,
278
+ medium: 1,
279
+ high: 2,
280
+ };
281
+ const riskOrdinal: Record<string, number> = {
282
+ [RiskLevel.Low]: 0,
283
+ [RiskLevel.Medium]: 1,
284
+ [RiskLevel.High]: 2,
253
285
  };
286
+ const withinThreshold =
287
+ (riskOrdinal[riskLevel] ?? 2) <=
288
+ (thresholdOrdinal[bgThreshold] ?? 0);
289
+ if (withinThreshold) {
290
+ log.info(
291
+ { toolName: name, riskLevel },
292
+ "Auto-approving for non-interactive guardian session",
293
+ );
294
+ return {
295
+ allowed: true,
296
+ decision: "guardian_auto_approve",
297
+ riskLevel,
298
+ riskMeta,
299
+ };
300
+ }
254
301
  }
255
302
 
256
303
  // Non-interactive sessions have no client to respond to prompts -
@@ -280,6 +327,7 @@ export class PermissionChecker {
280
327
  decision: "denied",
281
328
  riskLevel,
282
329
  content: `Permission denied: tool "${name}" requires user approval but no interactive client is connected. The tool was not executed. To allow this tool in non-interactive sessions, add a trust rule via permission settings.`,
330
+ riskMeta,
283
331
  };
284
332
  }
285
333
 
@@ -304,7 +352,12 @@ export class PermissionChecker {
304
352
  },
305
353
  "Temporary approval override active - auto-approving without prompt",
306
354
  );
307
- return { allowed: true, decision: "temporary_override", riskLevel };
355
+ return {
356
+ allowed: true,
357
+ decision: "temporary_override",
358
+ riskLevel,
359
+ riskMeta,
360
+ };
308
361
  }
309
362
 
310
363
  const previewDiff = computePreviewDiff(name, input, context.workingDir);
@@ -353,13 +406,6 @@ export class PermissionChecker {
353
406
  persistentDecisionsAllowed: promptOptions.persistentDecisionsAllowed,
354
407
  });
355
408
 
356
- await getHookManager().trigger("permission-request", {
357
- toolName: name,
358
- input: sanitizeToolInput(name, input),
359
- riskLevel,
360
- conversationId: context.conversationId,
361
- });
362
-
363
409
  const response = await this.prompter.prompt(
364
410
  name,
365
411
  input,
@@ -374,6 +420,8 @@ export class PermissionChecker {
374
420
  promptOptions.temporaryOptionsAvailable,
375
421
  context.toolUseId,
376
422
  v2ForcePrompt,
423
+ riskReason,
424
+ getIsContainerized(),
377
425
  );
378
426
 
379
427
  const decision =
@@ -381,13 +429,6 @@ export class PermissionChecker {
381
429
  ? "deny"
382
430
  : response.decision;
383
431
 
384
- await getHookManager().trigger("permission-resolve", {
385
- toolName: name,
386
- decision,
387
- riskLevel,
388
- conversationId: context.conversationId,
389
- });
390
-
391
432
  if (decision === "deny") {
392
433
  const contextualDenial =
393
434
  typeof response.decisionContext === "string"
@@ -421,6 +462,7 @@ export class PermissionChecker {
421
462
  decision,
422
463
  riskLevel,
423
464
  content: denialMessage,
465
+ riskMeta,
424
466
  };
425
467
  }
426
468
 
@@ -470,6 +512,7 @@ export class PermissionChecker {
470
512
  decision,
471
513
  riskLevel,
472
514
  content: denialMessage,
515
+ riskMeta,
473
516
  };
474
517
  }
475
518
 
@@ -523,11 +566,17 @@ export class PermissionChecker {
523
566
  );
524
567
  }
525
568
 
526
- return { allowed: true, decision, riskLevel, wasPrompted: true };
569
+ return {
570
+ allowed: true,
571
+ decision,
572
+ riskLevel,
573
+ wasPrompted: true,
574
+ riskMeta,
575
+ };
527
576
  }
528
577
 
529
578
  // result.decision === 'allow'
530
- return { allowed: true, decision: "allow", riskLevel };
579
+ return { allowed: true, decision: "allow", riskLevel, riskMeta };
531
580
  } catch (err) {
532
581
  if (err instanceof Error) {
533
582
  (err as Error & { riskLevel?: string }).riskLevel = riskLevel;
@@ -34,16 +34,20 @@ export function buildPolicyContext(
34
34
 
35
35
  const executionContext = deriveExecutionContext(context);
36
36
 
37
+ const conversationId = context?.conversationId;
38
+
37
39
  if (tool.origin === "skill") {
38
40
  return {
39
41
  executionTarget: tool.executionTarget,
40
42
  ephemeralRules: ephemeralRules?.length ? ephemeralRules : undefined,
41
43
  executionContext,
44
+ conversationId,
42
45
  };
43
46
  }
44
47
 
45
48
  return {
46
49
  ephemeralRules: ephemeralRules?.length ? ephemeralRules : undefined,
47
50
  executionContext,
51
+ conversationId,
48
52
  };
49
53
  }
@@ -68,6 +68,12 @@ let coreToolsSnapshot: Map<string, Tool> | null = null;
68
68
  // Tools are only removed from the global registry when this drops to 0.
69
69
  const skillRefCount = new Map<string, number>();
70
70
 
71
+ // Plugin-tool refcount lives in its own namespace so plugin and skill IDs
72
+ // cannot collide in the ref map even if a plugin's `manifest.name` happens to
73
+ // match a skill id. Conflict detection on `tools` (keyed by tool name) is
74
+ // separate and covers the case of two extensions choosing the same tool name.
75
+ const pluginRefCount = new Map<string, number>();
76
+
71
77
  export function registerTool(tool: Tool): void {
72
78
  const existing = tools.get(tool.name);
73
79
  if (existing) {
@@ -108,10 +114,22 @@ export function registerSkillTools(newTools: Tool[]): Tool[] {
108
114
  );
109
115
  continue;
110
116
  }
111
- // Existing is also a skill tool - only allow replacement from the same owner.
112
- if (existing.ownerSkillId !== tool.ownerSkillId) {
117
+ // Existing is from a different origin (plugin/mcp) or a different
118
+ // skill skill tools can only replace themselves (hot-reload).
119
+ if (
120
+ existing.origin !== "skill" ||
121
+ existing.ownerSkillId !== tool.ownerSkillId
122
+ ) {
123
+ const owner =
124
+ existing.origin === "skill"
125
+ ? `skill "${existing.ownerSkillId}"`
126
+ : existing.origin === "plugin"
127
+ ? `plugin "${existing.ownerPluginId}"`
128
+ : existing.origin === "mcp"
129
+ ? `MCP server "${existing.ownerMcpServerId}"`
130
+ : `${existing.origin ?? "unknown"}-origin tool`;
113
131
  throw new Error(
114
- `Skill tool "${tool.name}" is already registered by skill "${existing.ownerSkillId}"`,
132
+ `Skill tool "${tool.name}" is already registered by ${owner}`,
115
133
  );
116
134
  }
117
135
  }
@@ -136,6 +154,111 @@ export function registerSkillTools(newTools: Tool[]): Tool[] {
136
154
  return accepted;
137
155
  }
138
156
 
157
+ /**
158
+ * Register tools contributed by a plugin. Stamps `origin: "plugin"` and
159
+ * `ownerPluginId: pluginName` on every incoming tool so plugin ownership is
160
+ * tracked in a namespace disjoint from skill tools — if a plugin's
161
+ * `manifest.name` happens to match a skill id, the two do not share refcount
162
+ * state or conflict-detection paths.
163
+ *
164
+ * Conflict handling mirrors {@link registerSkillTools}: collisions with core
165
+ * tools log a warning and skip; collisions with tools owned by a different
166
+ * plugin, skill, or MCP server throw; re-registering the same plugin's own
167
+ * tool (hot reload) is allowed.
168
+ *
169
+ * The stamp is authoritative: any pre-existing `origin` / `ownerPluginId` /
170
+ * `ownerSkillId` / `ownerMcpServerId` fields on the incoming tools are
171
+ * overwritten so the bootstrap cannot be spoofed into claiming tools on
172
+ * behalf of an unrelated extension.
173
+ */
174
+ export function registerPluginTools(
175
+ pluginName: string,
176
+ newTools: Tool[],
177
+ ): Tool[] {
178
+ const stamped: Tool[] = newTools.map((tool) => ({
179
+ ...tool,
180
+ origin: "plugin" as const,
181
+ ownerPluginId: pluginName,
182
+ ownerSkillId: undefined,
183
+ ownerMcpServerId: undefined,
184
+ }));
185
+
186
+ const accepted: Tool[] = [];
187
+ for (const tool of stamped) {
188
+ const existing = tools.get(tool.name);
189
+ if (existing) {
190
+ const existingIsCore = existing.origin === "core" || !existing.origin;
191
+ if (existingIsCore) {
192
+ log.warn(
193
+ { toolName: tool.name, pluginName },
194
+ `Plugin "${pluginName}" tried to register tool "${tool.name}" which conflicts with a core tool. Skipping.`,
195
+ );
196
+ continue;
197
+ }
198
+ if (existing.origin === "plugin") {
199
+ if (existing.ownerPluginId !== pluginName) {
200
+ throw new Error(
201
+ `Plugin tool "${tool.name}" is already registered by plugin "${existing.ownerPluginId}"`,
202
+ );
203
+ }
204
+ // Same plugin re-registering its own tool (hot reload) — allow.
205
+ } else {
206
+ // Conflict with a skill or MCP-owned tool.
207
+ throw new Error(
208
+ `Plugin "${pluginName}" tried to register tool "${tool.name}" which conflicts with a ${existing.origin}-origin tool`,
209
+ );
210
+ }
211
+ }
212
+ accepted.push(tool);
213
+ }
214
+
215
+ for (const tool of accepted) {
216
+ tools.set(tool.name, tool);
217
+ log.info(
218
+ { name: tool.name, ownerPluginId: tool.ownerPluginId },
219
+ "Plugin tool registered",
220
+ );
221
+ }
222
+
223
+ if (accepted.length > 0) {
224
+ pluginRefCount.set(pluginName, (pluginRefCount.get(pluginName) ?? 0) + 1);
225
+ }
226
+
227
+ return accepted;
228
+ }
229
+
230
+ /**
231
+ * Decrement the reference count for a plugin and remove its tools only when
232
+ * no more references remain. Safe to call when the plugin never contributed
233
+ * tools (no-op).
234
+ */
235
+ export function unregisterPluginTools(pluginName: string): void {
236
+ const current = pluginRefCount.get(pluginName) ?? 0;
237
+ if (current > 1) {
238
+ pluginRefCount.set(pluginName, current - 1);
239
+ log.info(
240
+ { pluginName, remaining: current - 1 },
241
+ "Decremented plugin ref count, tools kept",
242
+ );
243
+ return;
244
+ }
245
+
246
+ pluginRefCount.delete(pluginName);
247
+ for (const [name, tool] of tools) {
248
+ if (tool.origin === "plugin" && tool.ownerPluginId === pluginName) {
249
+ tools.delete(name);
250
+ log.info({ name, pluginName }, "Plugin tool unregistered");
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Return the current reference count for a plugin's tools. Exposed for testing.
257
+ */
258
+ export function getPluginRefCount(pluginName: string): number {
259
+ return pluginRefCount.get(pluginName) ?? 0;
260
+ }
261
+
139
262
  /**
140
263
  * Decrement the reference count for a skill and remove its tools only when
141
264
  * no more sessions reference them.
@@ -190,6 +313,17 @@ export function registerMcpTools(newTools: Tool[]): Tool[] {
190
313
  );
191
314
  continue;
192
315
  }
316
+ if (existing.origin === "plugin") {
317
+ log.warn(
318
+ {
319
+ toolName: tool.name,
320
+ serverId: tool.ownerMcpServerId,
321
+ pluginName: existing.ownerPluginId,
322
+ },
323
+ `MCP server "${tool.ownerMcpServerId}" tried to register tool "${tool.name}" which conflicts with plugin tool from "${existing.ownerPluginId}". Skipping.`,
324
+ );
325
+ continue;
326
+ }
193
327
  if (
194
328
  existing.origin === "mcp" &&
195
329
  existing.ownerMcpServerId !== tool.ownerMcpServerId
@@ -341,6 +475,7 @@ export async function initializeTools(): Promise<void> {
341
475
  coreToolsSnapshot = new Map<string, Tool>();
342
476
  for (const [name, tool] of tools) {
343
477
  if (tool.origin === "skill") continue;
478
+ if (tool.origin === "plugin") continue;
344
479
  // Exclude pre-existing tools not declared in the manifest
345
480
  if (preExisting.has(name) && !manifestToolNames.has(name)) continue;
346
481
  coreToolsSnapshot.set(name, tool);
@@ -363,6 +498,7 @@ export async function initializeTools(): Promise<void> {
363
498
  export function __resetRegistryForTesting(): void {
364
499
  tools.clear();
365
500
  skillRefCount.clear();
501
+ pluginRefCount.clear();
366
502
 
367
503
  if (coreToolsSnapshot) {
368
504
  for (const [name, tool] of coreToolsSnapshot) {
@@ -379,4 +515,5 @@ export function __resetRegistryForTesting(): void {
379
515
  export function __clearRegistryForTesting(): void {
380
516
  tools.clear();
381
517
  skillRefCount.clear();
518
+ pluginRefCount.clear();
382
519
  }
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../schedule/schedule-store.js";
14
14
  import type { ToolContext, ToolExecutionResult } from "../types.js";
15
15
 
16
- const VALID_MODES: ScheduleMode[] = ["notify", "execute"];
16
+ const VALID_MODES: ScheduleMode[] = ["notify", "execute", "script"];
17
17
  const VALID_ROUTING_INTENTS: RoutingIntent[] = [
18
18
  "single_channel",
19
19
  "multi_channel",
@@ -33,7 +33,8 @@ export async function executeScheduleCreate(
33
33
  }
34
34
  const name = input.name as string;
35
35
  const timezone = (input.timezone as string) ?? null;
36
- const message = input.message as string;
36
+ const message = (input.message as string) ?? "";
37
+ const script = (input.script as string) ?? null;
37
38
  const enabled = (input.enabled as boolean) ?? true;
38
39
  const fireAt = input.fire_at as string | undefined;
39
40
  const mode = (input.mode as ScheduleMode | undefined) ?? "execute";
@@ -50,12 +51,6 @@ export async function executeScheduleCreate(
50
51
  isError: true,
51
52
  };
52
53
  }
53
- if (!message || typeof message !== "string") {
54
- return {
55
- content: "Error: message is required and must be a string",
56
- isError: true,
57
- };
58
- }
59
54
 
60
55
  // Validate mode
61
56
  if (!VALID_MODES.includes(mode)) {
@@ -65,6 +60,24 @@ export async function executeScheduleCreate(
65
60
  };
66
61
  }
67
62
 
63
+ // Mode-specific field validation
64
+ if (mode === "script") {
65
+ if (!script || typeof script !== "string") {
66
+ return {
67
+ content:
68
+ "Error: script is required for script mode and must be a non-empty string",
69
+ isError: true,
70
+ };
71
+ }
72
+ } else {
73
+ if (!message || typeof message !== "string") {
74
+ return {
75
+ content: "Error: message is required and must be a string",
76
+ isError: true,
77
+ };
78
+ }
79
+ }
80
+
68
81
  // Validate routing_intent
69
82
  if (
70
83
  routingIntent !== undefined &&
@@ -107,6 +120,7 @@ export async function executeScheduleCreate(
107
120
  cronExpression: null,
108
121
  timezone,
109
122
  message,
123
+ script,
110
124
  enabled,
111
125
  syntax: "cron",
112
126
  expression: null,
@@ -185,6 +199,7 @@ export async function executeScheduleCreate(
185
199
  cronExpression: resolved.expression,
186
200
  timezone,
187
201
  message,
202
+ script,
188
203
  enabled,
189
204
  syntax: resolved.syntax,
190
205
  expression: resolved.expression,
@@ -16,7 +16,7 @@ import {
16
16
  } from "../../schedule/schedule-store.js";
17
17
  import type { ToolContext, ToolExecutionResult } from "../types.js";
18
18
 
19
- const VALID_MODES: ScheduleMode[] = ["notify", "execute"];
19
+ const VALID_MODES: ScheduleMode[] = ["notify", "execute", "script"];
20
20
  const VALID_ROUTING_INTENTS: RoutingIntent[] = [
21
21
  "single_channel",
22
22
  "multi_channel",
@@ -66,6 +66,7 @@ export async function executeScheduleUpdate(
66
66
  if (input.name !== undefined) updates.name = input.name;
67
67
  if (input.timezone !== undefined) updates.timezone = input.timezone;
68
68
  if (input.message !== undefined) updates.message = input.message;
69
+ if (input.script !== undefined) updates.script = input.script;
69
70
  if (input.enabled !== undefined) updates.enabled = input.enabled;
70
71
 
71
72
  // Mode validation and pass-through
@@ -163,6 +164,7 @@ export async function executeScheduleUpdate(
163
164
  cronExpression?: string;
164
165
  timezone?: string | null;
165
166
  message?: string;
167
+ script?: string | null;
166
168
  enabled?: boolean;
167
169
  syntax?: ScheduleSyntax;
168
170
  expression?: string;
@@ -1,5 +1,4 @@
1
1
  import { getConfig } from "../config/loader.js";
2
- import { getHookManager } from "../hooks/manager.js";
3
2
  import { PermissionPrompter } from "../permissions/prompter.js";
4
3
  import { RiskLevel } from "../permissions/types.js";
5
4
  import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
@@ -46,10 +45,6 @@ export class SecretDetectionHandler {
46
45
  context: ToolContext,
47
46
  event: ToolLifecycleEvent,
48
47
  ) => void,
49
- sanitizeToolInput: (
50
- toolName: string,
51
- input: Record<string, unknown>,
52
- ) => Record<string, unknown>,
53
48
  ): Promise<{ result: ToolExecutionResult; earlyReturn: boolean }> {
54
49
  const sdConfig = getConfig().secretDetection;
55
50
  if (!sdConfig.enabled || execResult.isError) {
@@ -108,7 +103,6 @@ export class SecretDetectionHandler {
108
103
  decision,
109
104
  startTime,
110
105
  emitLifecycleEvent,
111
- sanitizeToolInput,
112
106
  );
113
107
  }
114
108
 
@@ -124,7 +118,6 @@ export class SecretDetectionHandler {
124
118
  decision,
125
119
  startTime,
126
120
  emitLifecycleEvent,
127
- sanitizeToolInput,
128
121
  );
129
122
  }
130
123
 
@@ -210,10 +203,6 @@ export class SecretDetectionHandler {
210
203
  context: ToolContext,
211
204
  event: ToolLifecycleEvent,
212
205
  ) => void,
213
- sanitizeToolInput: (
214
- toolName: string,
215
- input: Record<string, unknown>,
216
- ) => Record<string, unknown>,
217
206
  ): { result: ToolExecutionResult; earlyReturn: boolean } {
218
207
  const types = [...new Set(allMatches.map((m) => m.type))].join(", ");
219
208
  const blockedContent = `Tool output blocked: detected ${allMatches.length} potential secret(s) (${types}). Configure secretDetection.action to "redact" or "prompt" to allow output.`;
@@ -237,15 +226,6 @@ export class SecretDetectionHandler {
237
226
  result: blockedResult,
238
227
  });
239
228
 
240
- void getHookManager().trigger("post-tool-execute", {
241
- toolName: name,
242
- input: sanitizeToolInput(name, input),
243
- riskLevel,
244
- isError: true,
245
- durationMs,
246
- conversationId: context.conversationId,
247
- });
248
-
249
229
  return { result: blockedResult, earlyReturn: true };
250
230
  }
251
231
 
@@ -263,10 +243,6 @@ export class SecretDetectionHandler {
263
243
  context: ToolContext,
264
244
  event: ToolLifecycleEvent,
265
245
  ) => void,
266
- sanitizeToolInput: (
267
- toolName: string,
268
- input: Record<string, unknown>,
269
- ) => Record<string, unknown>,
270
246
  ): Promise<{ result: ToolExecutionResult; earlyReturn: boolean }> {
271
247
  const types = [...new Set(allMatches.map((m) => m.type))].join(", ");
272
248
 
@@ -288,15 +264,6 @@ export class SecretDetectionHandler {
288
264
  durationMs,
289
265
  });
290
266
 
291
- void getHookManager().trigger("post-tool-execute", {
292
- toolName: name,
293
- input: sanitizeToolInput(name, input),
294
- riskLevel,
295
- isError: true,
296
- durationMs,
297
- conversationId: context.conversationId,
298
- });
299
-
300
267
  return {
301
268
  result: { content: blockedContent, isError: true },
302
269
  earlyReturn: true,
@@ -322,15 +289,6 @@ export class SecretDetectionHandler {
322
289
  durationMs,
323
290
  });
324
291
 
325
- void getHookManager().trigger("post-tool-execute", {
326
- toolName: name,
327
- input: sanitizeToolInput(name, input),
328
- riskLevel,
329
- isError: true,
330
- durationMs,
331
- conversationId: context.conversationId,
332
- });
333
-
334
292
  return {
335
293
  result: { content: blockedContent, isError: true },
336
294
  earlyReturn: true,
@@ -389,15 +347,6 @@ export class SecretDetectionHandler {
389
347
  durationMs,
390
348
  });
391
349
 
392
- void getHookManager().trigger("post-tool-execute", {
393
- toolName: name,
394
- input: sanitizeToolInput(name, input),
395
- riskLevel,
396
- isError: true,
397
- durationMs,
398
- conversationId: context.conversationId,
399
- });
400
-
401
350
  return {
402
351
  result: { content: blockedContent, isError: true },
403
352
  earlyReturn: true,