@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
@@ -1058,22 +1058,43 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
1058
1058
  expect(candidates[2].kind).toBe("local");
1059
1059
  });
1060
1060
 
1061
- test("macOS turn with proxy unavailable skips desktop-auto cdp-inspect (extension intent)", () => {
1061
+ test("macOS turn with registry-routed proxy unavailable skips desktop-auto cdp-inspect (extension intent)", () => {
1062
1062
  const fakeProxy = makeUnavailableProxy();
1063
1063
  const ctx = makeContext({
1064
1064
  conversationId: "macos-proxy-unavailable-no-inspect",
1065
1065
  hostBrowserProxy: fakeProxy,
1066
1066
  transportInterface: "macos",
1067
+ hostBrowserRegistryRouted: true,
1067
1068
  });
1068
1069
 
1069
1070
  const candidates = buildCandidateList(ctx);
1070
1071
 
1071
1072
  // Should only include local -- cdp-inspect is suppressed because extension
1072
- // transport is expected (proxy exists) but temporarily unavailable.
1073
+ // transport is expected (registry-routed proxy) but temporarily unavailable.
1073
1074
  expect(candidates.length).toBe(1);
1074
1075
  expect(candidates[0].kind).toBe("local");
1075
1076
  });
1076
1077
 
1078
+ test("macOS turn with SSE-backed proxy unavailable still includes desktop-auto cdp-inspect", () => {
1079
+ const fakeProxy = makeUnavailableProxy();
1080
+ const ctx = makeContext({
1081
+ conversationId: "macos-sse-proxy-unavailable-inspect-allowed",
1082
+ hostBrowserProxy: fakeProxy,
1083
+ transportInterface: "macos",
1084
+ // hostBrowserRegistryRouted is NOT set -- SSE-backed proxy
1085
+ });
1086
+
1087
+ const candidates = buildCandidateList(ctx);
1088
+
1089
+ // SSE-backed proxy that is unavailable (non-interactive turn) should NOT
1090
+ // suppress cdp-inspect -- the SSE proxy was never expected to service
1091
+ // browser requests, so cdp-inspect remains available as a fallback.
1092
+ expect(candidates.length).toBe(2);
1093
+ expect(candidates[0].kind).toBe("cdp-inspect");
1094
+ expect(candidates[0].reason).toContain("desktopAuto");
1095
+ expect(candidates[1].kind).toBe("local");
1096
+ });
1097
+
1077
1098
  test("macOS turn with no proxy still includes desktop-auto cdp-inspect", () => {
1078
1099
  const ctx = makeContext({
1079
1100
  conversationId: "macos-no-proxy-inspect-allowed",
@@ -1242,17 +1263,19 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
1242
1263
  expect(candidates[0].kind).toBe("local");
1243
1264
  });
1244
1265
 
1245
- test("macOS turn with proxy unavailable routes to local without trying cdp-inspect", async () => {
1266
+ test("macOS turn with registry-routed proxy unavailable routes to local without trying cdp-inspect", async () => {
1246
1267
  const fakeProxy = makeUnavailableProxy();
1247
1268
  const ctx = makeContext({
1248
1269
  conversationId: "macos-proxy-unavail-route",
1249
1270
  hostBrowserProxy: fakeProxy,
1250
1271
  transportInterface: "macos",
1272
+ hostBrowserRegistryRouted: true,
1251
1273
  });
1252
1274
 
1253
1275
  const client = getCdpClient(ctx);
1254
1276
 
1255
1277
  // Should go straight to local -- no cdp-inspect candidate inserted
1278
+ // because the registry-routed extension was expected but is unavailable.
1256
1279
  expect(client.kind).toBe("local");
1257
1280
  const result = await client.send<{ ok: boolean; via: string }>(
1258
1281
  "Page.navigate",
@@ -1264,6 +1287,29 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
1264
1287
  client.dispose();
1265
1288
  });
1266
1289
 
1290
+ test("macOS turn with SSE-backed proxy unavailable falls through to cdp-inspect", async () => {
1291
+ const fakeProxy = makeUnavailableProxy();
1292
+ const ctx = makeContext({
1293
+ conversationId: "macos-sse-proxy-unavail-inspect",
1294
+ hostBrowserProxy: fakeProxy,
1295
+ transportInterface: "macos",
1296
+ // hostBrowserRegistryRouted is NOT set -- SSE-backed proxy
1297
+ });
1298
+
1299
+ const client = getCdpClient(ctx);
1300
+
1301
+ // SSE-backed proxy unavailable (non-interactive turn) should NOT
1302
+ // suppress cdp-inspect -- it falls through to desktop-auto cdp-inspect.
1303
+ expect(client.kind).toBe("cdp-inspect");
1304
+ const result = await client.send<{ ok: boolean; via: string }>(
1305
+ "Page.navigate",
1306
+ );
1307
+ expect(result).toEqual({ ok: true, via: "cdp-inspect" });
1308
+ expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
1309
+ expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
1310
+ client.dispose();
1311
+ });
1312
+
1267
1313
  test("explicit config cdp-inspect failure does NOT record desktop-auto cooldown", async () => {
1268
1314
  cdpInspectEnabled = true;
1269
1315
 
@@ -1991,3 +2037,100 @@ describe("no-fallback guarantees", () => {
1991
2037
  expect(fallbackLogs.length).toBe(0);
1992
2038
  });
1993
2039
  });
2040
+
2041
+ // ── macOS host-browser proxy backend selection ─────────────────────────
2042
+ //
2043
+ // Verify that macOS turns can use the host browser proxy without requiring
2044
+ // extension registry connectivity. When a HostBrowserProxy is provisioned
2045
+ // via the SSE sender path (no extension), the factory should select
2046
+ // extension as the top candidate (because hostBrowserProxy is available).
2047
+ // When both proxy and fallback backends exist, selection is deterministic:
2048
+ // extension > cdp-inspect > local.
2049
+
2050
+ describe("macOS host-browser proxy without extension registry", () => {
2051
+ beforeEach(() => {
2052
+ createExtensionCdpClientMock.mockClear();
2053
+ createLocalCdpClientMock.mockClear();
2054
+ createCdpInspectClientMock.mockClear();
2055
+ lastExtensionClient = undefined;
2056
+ lastLocalClient = undefined;
2057
+ lastCdpInspectClient = undefined;
2058
+ cdpInspectEnabled = false;
2059
+ desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
2060
+ _resetDesktopAutoCooldown();
2061
+ logWarnCalls.length = 0;
2062
+ logDebugCalls.length = 0;
2063
+ });
2064
+
2065
+ test("macOS turn with SSE-provisioned hostBrowserProxy selects extension backend", async () => {
2066
+ // Simulates macOS provisioning a HostBrowserProxy via SSE (no extension
2067
+ // registry connection). The proxy is available so extension is selected.
2068
+ const fakeProxy = makeAvailableProxy();
2069
+ const ctx = makeContext({
2070
+ conversationId: "macos-sse-proxy",
2071
+ hostBrowserProxy: fakeProxy,
2072
+ transportInterface: "macos",
2073
+ });
2074
+
2075
+ const client = getCdpClient(ctx);
2076
+
2077
+ expect(client.kind).toBe("extension");
2078
+ const result = await client.send<{ ok: boolean; via: string }>(
2079
+ "Page.navigate",
2080
+ { url: "https://example.com" },
2081
+ );
2082
+ expect(result).toEqual({ ok: true, via: "extension" });
2083
+ expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
2084
+ });
2085
+
2086
+ test("macOS turn with both proxy and cdp-inspect produces deterministic 3-candidate chain", () => {
2087
+ const fakeProxy = makeAvailableProxy();
2088
+ const ctx = makeContext({
2089
+ conversationId: "macos-deterministic",
2090
+ hostBrowserProxy: fakeProxy,
2091
+ transportInterface: "macos",
2092
+ });
2093
+
2094
+ const candidates = buildCandidateList(ctx);
2095
+
2096
+ // Deterministic order: extension > cdp-inspect (desktop-auto) > local
2097
+ expect(candidates.length).toBe(3);
2098
+ expect(candidates[0].kind).toBe("extension");
2099
+ expect(candidates[1].kind).toBe("cdp-inspect");
2100
+ expect(candidates[2].kind).toBe("local");
2101
+ });
2102
+
2103
+ test("macOS turn without proxy falls through to cdp-inspect then local", () => {
2104
+ const ctx = makeContext({
2105
+ conversationId: "macos-no-proxy-fallback",
2106
+ transportInterface: "macos",
2107
+ });
2108
+
2109
+ const candidates = buildCandidateList(ctx);
2110
+
2111
+ // No proxy => skip extension, desktop-auto cdp-inspect + local
2112
+ expect(candidates.length).toBe(2);
2113
+ expect(candidates[0].kind).toBe("cdp-inspect");
2114
+ expect(candidates[1].kind).toBe("local");
2115
+ });
2116
+
2117
+ test("non-macOS interface with proxy still selects extension (unchanged behavior)", async () => {
2118
+ // Verify non-macOS interfaces are unaffected by the macOS host-browser
2119
+ // enablement — proxy presence drives extension selection regardless of
2120
+ // interface.
2121
+ const fakeProxy = makeAvailableProxy();
2122
+ const ctx = makeContext({
2123
+ conversationId: "non-macos-proxy",
2124
+ hostBrowserProxy: fakeProxy,
2125
+ transportInterface: "cli",
2126
+ });
2127
+
2128
+ const client = getCdpClient(ctx);
2129
+
2130
+ expect(client.kind).toBe("extension");
2131
+ const result = await client.send<{ ok: boolean; via: string }>(
2132
+ "Page.navigate",
2133
+ );
2134
+ expect(result).toEqual({ ok: true, via: "extension" });
2135
+ });
2136
+ });
@@ -1,10 +1,30 @@
1
1
  import type { HostBrowserProxy } from "../../../daemon/host-browser-proxy.js";
2
2
  import { getLogger } from "../../../util/logger.js";
3
+ import type { CdpErrorCode } from "./errors.js";
3
4
  import { CdpError } from "./errors.js";
4
5
  import type { CdpClientKind, ScopedCdpClient } from "./types.js";
5
6
 
6
7
  const log = getLogger("extension-cdp-client");
7
8
 
9
+ /**
10
+ * Transport-level error codes that the host_browser dispatcher may
11
+ * embed in a structured `{ code, message }` error envelope. When the
12
+ * `code` field of a parsed error object matches one of these values,
13
+ * the error is classified as `transport_error` so the factory's
14
+ * failover logic can try the next backend candidate.
15
+ *
16
+ * Codes that are NOT in this set are treated as CDP command-level
17
+ * failures (`cdp_error`) and propagate without failover.
18
+ */
19
+ const TRANSPORT_ERROR_CODES = new Set([
20
+ "transport_error",
21
+ "unreachable",
22
+ "timeout",
23
+ "non_loopback",
24
+ "cdp_session_not_found",
25
+ "cancelled",
26
+ ]);
27
+
8
28
  /**
9
29
  * CdpClient backed by HostBrowserProxy. Each `send` becomes a
10
30
  * host_browser_request / host_browser_result round-trip over the
@@ -102,11 +122,21 @@ export class ExtensionCdpClient implements ScopedCdpClient {
102
122
  typeof (parsedError as { message: unknown }).message === "string" &&
103
123
  (parsedError as { message: string }).message) ||
104
124
  `CDP error for ${method}`;
125
+
126
+ // Detect structured transport error envelopes from the
127
+ // host_browser dispatcher. When the parsed error object
128
+ // carries a `code` field that matches a known transport-level
129
+ // code, classify the error as `transport_error` so the
130
+ // factory can trigger failover to the next backend candidate.
131
+ // All other structured errors remain `cdp_error` since they
132
+ // represent command-level CDP failures that would not benefit
133
+ // from switching transports.
134
+ const errorCode = classifyHostBrowserError(parsedError);
105
135
  log.debug(
106
- { method, params, parsedError },
107
- "ExtensionCdpClient: CDP error",
136
+ { method, params, parsedError, classifiedAs: errorCode },
137
+ "ExtensionCdpClient: host_browser_result error",
108
138
  );
109
- throw new CdpError("cdp_error", msg, {
139
+ throw new CdpError(errorCode, msg, {
110
140
  cdpMethod: method,
111
141
  cdpParams: params,
112
142
  underlying: parsedError,
@@ -139,6 +169,27 @@ export class ExtensionCdpClient implements ScopedCdpClient {
139
169
  }
140
170
  }
141
171
 
172
+ /**
173
+ * Classify a parsed host_browser_result error envelope as either a
174
+ * transport-level error (`transport_error`) or a command-level CDP
175
+ * failure (`cdp_error`).
176
+ *
177
+ * Structured envelopes from the host_browser dispatcher carry a
178
+ * `code` string field (e.g. `"transport_error"`, `"unreachable"`,
179
+ * `"timeout"`, `"non_loopback"`, `"cdp_session_not_found"`,
180
+ * `"cancelled"`). When the code matches a known
181
+ * transport-level value, the error is eligible for factory failover.
182
+ * All other codes (or missing codes) are treated as CDP command
183
+ * errors that should propagate without failover.
184
+ */
185
+ function classifyHostBrowserError(parsed: unknown): CdpErrorCode {
186
+ if (typeof parsed !== "object" || parsed === null) return "cdp_error";
187
+ if (!("code" in parsed)) return "cdp_error";
188
+ const code = (parsed as { code: unknown }).code;
189
+ if (typeof code !== "string") return "cdp_error";
190
+ return TRANSPORT_ERROR_CODES.has(code) ? "transport_error" : "cdp_error";
191
+ }
192
+
142
193
  export function createExtensionCdpClient(
143
194
  proxy: HostBrowserProxy,
144
195
  conversationId: string,
@@ -349,15 +349,26 @@ export function buildCandidateList(context: ToolContext): BackendCandidate[] {
349
349
  cdpInspectConfig.desktopAuto.enabled
350
350
  ) {
351
351
  // macOS desktop-auto: include cdp-inspect as a candidate unless:
352
- // (a) the hostBrowserProxy exists but is temporarily unavailable
353
- // (extension transport expected but transiently disconnected --
354
- // inserting cdp-inspect here would cause a silent takeover), or
352
+ // (a) the hostBrowserProxy is registry-routed (extension-backed) and
353
+ // temporarily unavailable the extension transport was explicitly
354
+ // expected and the disconnection is transient, so inserting
355
+ // cdp-inspect would cause a silent takeover. Only applies when
356
+ // `hostBrowserRegistryRouted` is true (set when
357
+ // `hostBrowserSenderOverride` was wired at turn-start).
358
+ // SSE-backed proxies (macOS without an extension connection) that
359
+ // report unavailable (e.g. non-interactive turns where
360
+ // clientConnected=false) should NOT suppress cdp-inspect — the
361
+ // SSE proxy was never expected to service browser requests.
355
362
  // (b) the cooldown from a recent failure is still active.
356
363
  //
357
364
  // When no hostBrowserProxy is present at all (extension not
358
365
  // provisioned for this conversation), cdp-inspect remains available
359
366
  // as a fallback per the desktop-auto contract.
360
- if (hostBrowserProxy && !hostBrowserProxy.isAvailable()) {
367
+ if (
368
+ hostBrowserProxy &&
369
+ !hostBrowserProxy.isAvailable() &&
370
+ context.hostBrowserRegistryRouted
371
+ ) {
361
372
  log.debug(
362
373
  { conversationId },
363
374
  "CDP factory: desktop-auto cdp-inspect skipped (extension transport expected but temporarily unavailable)",
@@ -1,11 +1,18 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
+ import { parseChannelId } from "../channels/types.js";
3
4
  import { getConfig } from "../config/loader.js";
4
5
  import { bridgeCesApproval } from "../credential-execution/approval-bridge.js";
5
6
  import { isCesShellLockdownEnabled } from "../credential-execution/feature-gates.js";
6
- import { getHookManager } from "../hooks/manager.js";
7
7
  import { PermissionPrompter } from "../permissions/prompter.js";
8
8
  import { RiskLevel } from "../permissions/types.js";
9
+ import { runPipeline } from "../plugins/pipeline.js";
10
+ import { getMiddlewaresFor } from "../plugins/registry.js";
11
+ import type {
12
+ ToolExecuteArgs,
13
+ ToolExecuteResult,
14
+ TurnContext,
15
+ } from "../plugins/types.js";
9
16
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
10
17
  import { redactSensitiveFields } from "../security/redaction.js";
11
18
  import { TokenExpiredError } from "../security/token-manager.js";
@@ -46,6 +53,59 @@ export class ToolExecutor {
46
53
  name: string,
47
54
  input: Record<string, unknown>,
48
55
  context: ToolContext,
56
+ /**
57
+ * Optional per-turn context threaded in by the agent loop. Production
58
+ * sites propagate the orchestrator-built `TurnContext` (real
59
+ * `conversationId`, trust cascade, attached `contextWindowManager`) so
60
+ * middleware registered on the `toolExecute` pipeline sees the same
61
+ * context every other pipeline slot uses. When omitted (CLI/test
62
+ * invocations that call `ToolExecutor.execute` directly), the executor
63
+ * synthesizes a fallback context from the {@link ToolContext}, which
64
+ * keeps pre-threading behavior intact for legacy callers.
65
+ */
66
+ turnContext?: TurnContext,
67
+ ): Promise<ToolExecutionResult> {
68
+ // Prefer the orchestrator-supplied `turnContext` so the pipeline sees
69
+ // the real conversation identity, per-turn trust, and context-window
70
+ // manager. When absent (CLI / test invocations that bypass the agent
71
+ // loop), synthesize a minimal context from the `ToolContext` — the
72
+ // same fallback the executor has used since the pipeline was added.
73
+ const turnCtx: TurnContext = turnContext ?? {
74
+ requestId: context.requestId ?? "",
75
+ conversationId: context.conversationId,
76
+ turnIndex: 0,
77
+ trust: {
78
+ sourceChannel: parseChannelId(context.executionChannel) ?? "vellum",
79
+ trustClass: context.trustClass,
80
+ },
81
+ };
82
+
83
+ const middlewares = getMiddlewaresFor("toolExecute");
84
+ const pipelineArgs: ToolExecuteArgs = { name, input, context };
85
+
86
+ // No pipeline-level timeout: `executeInternal` already wraps the real
87
+ // tool invocation in `executeWithTimeout`, which is the sole enforcer
88
+ // of the per-tool budget. Propagating `perToolTimeoutMs` to
89
+ // `runPipeline` made the pipeline race everything upstream of the
90
+ // tool call — permission checks, approval waits, middleware — against
91
+ // the same budget, so a slow human clicking "allow" produced a
92
+ // `PluginTimeoutError` thrown past `executeInternal`'s catch block,
93
+ // breaking the `execute()` never-throws contract. Letting the pipeline
94
+ // run untimed keeps the contract intact; runaway middleware is a
95
+ // plugin-health concern handled by per-plugin timeouts, not here.
96
+ return runPipeline<ToolExecuteArgs, ToolExecuteResult>(
97
+ "toolExecute",
98
+ middlewares,
99
+ (args) => this.executeInternal(args.name, args.input, args.context),
100
+ pipelineArgs,
101
+ turnCtx,
102
+ );
103
+ }
104
+
105
+ private async executeInternal(
106
+ name: string,
107
+ input: Record<string, unknown>,
108
+ context: ToolContext,
49
109
  ): Promise<ToolExecutionResult> {
50
110
  const startTime = Date.now();
51
111
  let decision = "allow";
@@ -112,6 +172,14 @@ export class ToolExecutor {
112
172
  // Exception: requireFreshApproval tools always go through the
113
173
  // permission check even when a grant was consumed - the grant does
114
174
  // not substitute for an interactive human review.
175
+ let permRiskMeta:
176
+ | {
177
+ riskLevel: string;
178
+ riskReason: string;
179
+ riskScopeOptions: Array<{ pattern: string; label: string }>;
180
+ isContainerized?: boolean;
181
+ }
182
+ | undefined;
115
183
  if (!gateResult.grantConsumed || context.requireFreshApproval) {
116
184
  // Check permissions via the extracted PermissionChecker
117
185
  const permResult = await this.permissionChecker.checkPermission(
@@ -121,16 +189,23 @@ export class ToolExecutor {
121
189
  context,
122
190
  executionTarget,
123
191
  (event) => emitLifecycleEvent(context, event),
124
- sanitizeToolInput,
125
192
  startTime,
126
193
  computePreviewDiff,
127
194
  );
128
195
 
129
196
  riskLevel = permResult.riskLevel;
130
197
  decision = permResult.decision;
198
+ permRiskMeta = permResult.riskMeta;
131
199
 
132
200
  if (!permResult.allowed) {
133
- return { content: permResult.content, isError: true };
201
+ return {
202
+ content: permResult.content,
203
+ isError: true,
204
+ riskLevel: permRiskMeta?.riskLevel,
205
+ riskReason: permRiskMeta?.riskReason,
206
+ riskScopeOptions: permRiskMeta?.riskScopeOptions,
207
+ isContainerized: permRiskMeta?.isContainerized,
208
+ };
134
209
  }
135
210
 
136
211
  if (permResult.wasPrompted) {
@@ -138,59 +213,11 @@ export class ToolExecutor {
138
213
  }
139
214
  }
140
215
 
141
- const hookResult = await getHookManager().trigger("pre-tool-execute", {
142
- toolName: name,
143
- input: sanitizeToolInput(name, input),
144
- riskLevel,
145
- decision,
146
- workingDir: context.workingDir,
147
- conversationId: context.conversationId,
148
- });
149
-
150
- if (hookResult.blocked) {
151
- const msg = `Tool execution blocked by hook "${hookResult.blockedBy}"`;
152
- const durationMs = Date.now() - startTime;
153
- emitLifecycleEvent(context, {
154
- type: "error",
155
- toolName: name,
156
- executionTarget,
157
- input,
158
- workingDir: context.workingDir,
159
- conversationId: context.conversationId,
160
- requestId: context.requestId,
161
- riskLevel,
162
- decision: "blocked",
163
- durationMs,
164
- errorMessage: msg,
165
- isExpected: true,
166
- errorCategory: "tool_failure",
167
- });
168
- return { content: msg, isError: true };
169
- }
170
-
171
- // Execute the tool - proxy tools delegate to an external resolver
216
+ // Execute the tool - proxy tools delegate to an external resolver.
217
+ // Use the shared per-tool timeout helper so the pipeline runner and
218
+ // the inner execute-with-timeout wrapper agree on the same budget.
172
219
  let execResult: ToolExecutionResult;
173
- let toolTimeoutMs: number;
174
- if (name === "bash" || name === "host_bash") {
175
- // Shell tools manage their own timeouts (SIGKILL on expiry).
176
- // Compute the same effective timeout so the executor wrapper
177
- // doesn't prematurely kill them with the generic toolExecutionTimeoutSec.
178
- const { shellDefaultTimeoutSec, shellMaxTimeoutSec } =
179
- getConfig().timeouts;
180
- const requestedSec =
181
- typeof input.timeout_seconds === "number"
182
- ? input.timeout_seconds
183
- : shellDefaultTimeoutSec;
184
- const shellTimeoutSec = Math.max(
185
- 1,
186
- Math.min(requestedSec, shellMaxTimeoutSec),
187
- );
188
- // Buffer so the shell's own timeout fires first and handles cleanup
189
- toolTimeoutMs = (shellTimeoutSec + 5) * 1000;
190
- } else {
191
- const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
192
- toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
193
- }
220
+ const toolTimeoutMs = computePerToolTimeoutMs(name, input);
194
221
 
195
222
  const execContext = context;
196
223
 
@@ -364,7 +391,6 @@ export class ToolExecutor {
364
391
  decision,
365
392
  startTime,
366
393
  emitLifecycleEvent,
367
- sanitizeToolInput,
368
394
  );
369
395
  if (secretResult.earlyReturn) {
370
396
  return secretResult.result;
@@ -388,14 +414,18 @@ export class ToolExecutor {
388
414
  result: safeResult,
389
415
  });
390
416
 
391
- void getHookManager().trigger("post-tool-execute", {
392
- toolName: name,
393
- input: sanitizeToolInput(name, input),
394
- riskLevel,
395
- isError: execResult.isError,
396
- durationMs,
397
- conversationId: context.conversationId,
398
- });
417
+ // Merge risk metadata from the classifier assessment cache onto the
418
+ // tool result so downstream consumers (AgentEvent → handleToolResult →
419
+ // ToolResult SSE message) can forward it to the client.
420
+ if (permRiskMeta) {
421
+ execResult = {
422
+ ...execResult,
423
+ riskLevel: permRiskMeta.riskLevel,
424
+ riskReason: permRiskMeta.riskReason,
425
+ riskScopeOptions: permRiskMeta.riskScopeOptions,
426
+ isContainerized: permRiskMeta.isContainerized,
427
+ };
428
+ }
399
429
 
400
430
  return execResult;
401
431
  } catch (err) {
@@ -446,15 +476,6 @@ export class ToolExecutor {
446
476
  errorStack: err instanceof Error ? err.stack : undefined,
447
477
  });
448
478
 
449
- void getHookManager().trigger("post-tool-execute", {
450
- toolName: name,
451
- input: sanitizeToolInput(name, input),
452
- riskLevel,
453
- isError: true,
454
- durationMs,
455
- conversationId: context.conversationId,
456
- });
457
-
458
479
  if (isExpected) {
459
480
  return { content: msg, isError: true };
460
481
  }
@@ -474,7 +495,38 @@ export { isSideEffectTool } from "./side-effects.js";
474
495
  export { PermissionChecker } from "./permission-checker.js";
475
496
 
476
497
  /**
477
- * Sanitize tool inputs before they are emitted in lifecycle events and hooks.
498
+ * Compute the effective per-tool execution timeout in milliseconds.
499
+ *
500
+ * Shell tools (`bash`, `host_bash`) manage their own timeouts with SIGKILL
501
+ * on expiry. We add a 5s buffer so the shell's own deadline fires first and
502
+ * handles cleanup before the executor wrapper trips. Non-shell tools use
503
+ * the generic `toolExecutionTimeoutSec` configuration value.
504
+ *
505
+ * Consumed by `executeInternal` via `executeWithTimeout`, which is the
506
+ * sole enforcer of the per-tool budget.
507
+ */
508
+ function computePerToolTimeoutMs(
509
+ name: string,
510
+ input: Record<string, unknown>,
511
+ ): number {
512
+ if (name === "bash" || name === "host_bash") {
513
+ const { shellDefaultTimeoutSec, shellMaxTimeoutSec } = getConfig().timeouts;
514
+ const requestedSec =
515
+ typeof input.timeout_seconds === "number"
516
+ ? input.timeout_seconds
517
+ : shellDefaultTimeoutSec;
518
+ const shellTimeoutSec = Math.max(
519
+ 1,
520
+ Math.min(requestedSec, shellMaxTimeoutSec),
521
+ );
522
+ return (shellTimeoutSec + 5) * 1000;
523
+ }
524
+ const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
525
+ return safeTimeoutMs(rawTimeoutSec);
526
+ }
527
+
528
+ /**
529
+ * Sanitize tool inputs before they are emitted in lifecycle events.
478
530
  * Applies recursive field-level redaction for known-sensitive keys.
479
531
  */
480
532
  function sanitizeToolInput(
@@ -81,6 +81,36 @@ const ALLOWED_HOST_PATTERNS: readonly string[] = (() => {
81
81
  return defaults;
82
82
  })();
83
83
 
84
+ /**
85
+ * Non-sensitive HTTP request headers that are safe to surface in the
86
+ * `network_request` approval prompt. Strict allowlist to keep Authorization,
87
+ * Cookie, X-Api-Key, and other custom credential-bearing headers off-screen.
88
+ */
89
+ const APPROVAL_HEADER_ALLOWLIST: readonly string[] = [
90
+ "content-type",
91
+ "content-length",
92
+ "user-agent",
93
+ "accept",
94
+ ];
95
+
96
+ /**
97
+ * Project an incoming header map onto {@link APPROVAL_HEADER_ALLOWLIST},
98
+ * collapsing multi-value arrays to a comma-joined string. Returns undefined
99
+ * when no headers are available (e.g. HTTPS CONNECT path).
100
+ */
101
+ function filterApprovalHeaders(
102
+ raw: Record<string, string | string[] | undefined> | undefined,
103
+ ): Record<string, string> | undefined {
104
+ if (!raw) return undefined;
105
+ const out: Record<string, string> = {};
106
+ for (const key of APPROVAL_HEADER_ALLOWLIST) {
107
+ const value = raw[key];
108
+ if (value === undefined) continue;
109
+ out[key] = Array.isArray(value) ? value.join(", ") : value;
110
+ }
111
+ return out;
112
+ }
113
+
84
114
  /**
85
115
  * Returns `true` when `hostname` matches any entry in
86
116
  * {@link ALLOWED_HOST_PATTERNS}.
@@ -292,12 +322,16 @@ function buildSessionStartHooks(): SessionStartHooks {
292
322
  return allKnownCache;
293
323
  }
294
324
 
295
- // Build the policy callback for HTTP/CONNECT request gating
325
+ // Build the policy callback for HTTP/CONNECT request gating.
326
+ // `method` / `reqHeaders` are populated for plain-HTTP proxied requests
327
+ // and undefined for HTTPS CONNECT tunnels (TLS not yet terminated).
296
328
  const policyCallback: PolicyCallback = async (
297
329
  hostname: string,
298
330
  port: number | null,
299
331
  reqPath: string,
300
332
  scheme: "http" | "https",
333
+ method?: string,
334
+ reqHeaders?: Record<string, string | string[] | undefined>,
301
335
  ) => {
302
336
  if (isAllowedHost(hostname)) {
303
337
  log.debug({ hostname }, "Allowing always-permitted host");
@@ -356,6 +390,8 @@ function buildSessionStartHooks(): SessionStartHooks {
356
390
  const approved = await managed.approvalCallback({
357
391
  decision,
358
392
  sessionId: managed.session.id,
393
+ method,
394
+ requestHeaders: filterApprovalHeaders(reqHeaders),
359
395
  });
360
396
  return approved ? {} : null;
361
397
  }