@vellumai/assistant 0.11.4 → 0.11.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -1496,6 +1496,17 @@ describe("stripInjectionsForCompaction memory/info wrapper matching", () => {
1496
1496
  // applyRuntimeInjections with nowScratchpad
1497
1497
  // ---------------------------------------------------------------------------
1498
1498
 
1499
+ /**
1500
+ * Personal-memory content (NOW.md, PKB, v3 cards) is gated on the actor's
1501
+ * trust class, so suites asserting that content have to name the actor they
1502
+ * are about. Left unset, the injector substitutes the low-trust fallback and
1503
+ * the assertion silently becomes a test of the gate instead of the content.
1504
+ */
1505
+ const GUARDIAN_TRUST_FIXTURE = {
1506
+ trustClass: "guardian",
1507
+ sourceChannel: "vellum",
1508
+ } as const;
1509
+
1499
1510
  describe("applyRuntimeInjections with nowScratchpad", () => {
1500
1511
  const baseMessages: Message[] = [
1501
1512
  {
@@ -1513,6 +1524,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
1513
1524
  seedNowScratchpad("Current focus: fix the bug");
1514
1525
  const { messages: result } = await applyRuntimeInjections(baseMessages, {
1515
1526
  conversationId: FALLBACK_CONVERSATION_ID,
1527
+ trust: GUARDIAN_TRUST_FIXTURE,
1516
1528
  });
1517
1529
 
1518
1530
  expect(result.length).toBe(1);
@@ -1527,6 +1539,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
1527
1539
  seedNowScratchpad("scratchpad notes");
1528
1540
  const { messages: result } = await applyRuntimeInjections(baseMessages, {
1529
1541
  conversationId: FALLBACK_CONVERSATION_ID,
1542
+ trust: GUARDIAN_TRUST_FIXTURE,
1530
1543
  });
1531
1544
 
1532
1545
  // Scratchpad comes first (before user content)
@@ -1542,6 +1555,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
1542
1555
  test("does not inject when the NOW.md file is absent", async () => {
1543
1556
  const { messages: result } = await applyRuntimeInjections(baseMessages, {
1544
1557
  conversationId: FALLBACK_CONVERSATION_ID,
1558
+ trust: GUARDIAN_TRUST_FIXTURE,
1545
1559
  });
1546
1560
 
1547
1561
  expect(result.length).toBe(1);
@@ -1552,6 +1566,7 @@ describe("applyRuntimeInjections with nowScratchpad", () => {
1552
1566
  seedNowScratchpad("Current focus: fix the bug");
1553
1567
  const { messages: result } = await applyRuntimeInjections(baseMessages, {
1554
1568
  conversationId: FALLBACK_CONVERSATION_ID,
1569
+ trust: GUARDIAN_TRUST_FIXTURE,
1555
1570
  mode: "minimal",
1556
1571
  });
1557
1572
 
@@ -2873,6 +2888,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
2873
2888
  function makePkbOptions(overrides: Record<string, unknown> = {}) {
2874
2889
  return {
2875
2890
  conversationId: FALLBACK_CONVERSATION_ID,
2891
+ trust: GUARDIAN_TRUST_FIXTURE,
2876
2892
  ...overrides,
2877
2893
  };
2878
2894
  }
@@ -3159,7 +3175,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
3159
3175
  // filtered out.
3160
3176
  const { messages: initialResult } = await applyRuntimeInjections(
3161
3177
  preCompactionMessages,
3162
- { conversationId: FALLBACK_CONVERSATION_ID },
3178
+ makePkbOptions(),
3163
3179
  );
3164
3180
  // Unwrap the injected reminder from the last user message.
3165
3181
  const initialTexts = extractTexts(initialResult);
@@ -3193,7 +3209,7 @@ describe("applyRuntimeInjections — PKB relevance hints", () => {
3193
3209
  // should be filtered, and beta (no longer "in context") should appear.
3194
3210
  const { messages: rebuiltResult } = await applyRuntimeInjections(
3195
3211
  postCompactionMessages,
3196
- { conversationId: FALLBACK_CONVERSATION_ID },
3212
+ makePkbOptions(),
3197
3213
  );
3198
3214
  const rebuiltTexts = extractTexts(rebuiltResult);
3199
3215
  const rebuiltReminder = rebuiltTexts.find(
@@ -6652,6 +6668,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
6652
6668
  pkbSearchThrows = null;
6653
6669
  const { blocks } = await applyRuntimeInjections(baseMessages, {
6654
6670
  conversationId: FALLBACK_CONVERSATION_ID,
6671
+ trust: GUARDIAN_TRUST_FIXTURE,
6655
6672
  mode: "full",
6656
6673
  });
6657
6674
 
@@ -6663,6 +6680,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
6663
6680
  seedPkbContent();
6664
6681
  const { blocks } = await applyRuntimeInjections(baseMessages, {
6665
6682
  conversationId: FALLBACK_CONVERSATION_ID,
6683
+ trust: GUARDIAN_TRUST_FIXTURE,
6666
6684
  mode: "minimal",
6667
6685
  });
6668
6686
 
@@ -6672,6 +6690,7 @@ describe("applyRuntimeInjections blocks.pkbSystemReminder", () => {
6672
6690
  test("not captured when PKB inactive", async () => {
6673
6691
  const { blocks } = await applyRuntimeInjections(baseMessages, {
6674
6692
  conversationId: FALLBACK_CONVERSATION_ID,
6693
+ trust: GUARDIAN_TRUST_FIXTURE,
6675
6694
  mode: "full",
6676
6695
  });
6677
6696
 
@@ -44,8 +44,24 @@ import { setSecureKeyAsync } from "../security/secure-keys.js";
44
44
  import { CredentialBroker } from "../tools/credentials/broker.js";
45
45
  import {
46
46
  _setMetadataPath,
47
+ type CredentialMetadata,
47
48
  upsertCredentialMetadata,
48
49
  } from "../tools/credentials/metadata-store.js";
50
+ import { serverUseDenialReason } from "../tools/credentials/tool-policy.js";
51
+
52
+ /** The shared policy's verdict for a state the broker must also deny. */
53
+ function sharedDenialReason(
54
+ metadata: CredentialMetadata,
55
+ toolName: string,
56
+ service: string,
57
+ field: string,
58
+ ): string {
59
+ const reason = serverUseDenialReason(metadata, toolName, service, field);
60
+ if (!reason) {
61
+ throw new Error("expected a denial reason from the shared policy");
62
+ }
63
+ return reason;
64
+ }
49
65
 
50
66
  // ---------------------------------------------------------------------------
51
67
  // Tests — serverUse (publish_page / unpublish_page regression)
@@ -518,9 +534,10 @@ describe("CredentialBroker.serverUseById", () => {
518
534
  if (result.success) {
519
535
  throw new Error("expected denial");
520
536
  }
521
- expect(result.reason).toContain("not allowed");
522
- expect(result.reason).toContain("unauthorized_tool");
523
- expect(result.reason).toContain("media_proxy");
537
+ // The broker passes the shared server-use policy's verdict through verbatim.
538
+ expect(result.reason).toBe(
539
+ sharedDenialReason(meta, "unauthorized_tool", "fal", "api_key"),
540
+ );
524
541
  });
525
542
 
526
543
  test("returns not found for unknown credential ID", async () => {
@@ -553,8 +570,10 @@ describe("CredentialBroker.serverUseById", () => {
553
570
  if (result.success) {
554
571
  throw new Error("expected denial");
555
572
  }
556
- expect(result.reason).toContain("domain restrictions");
557
- expect(result.reason).toContain("cannot be used server-side");
573
+ // The broker passes the shared server-use policy's verdict through verbatim.
574
+ expect(result.reason).toBe(
575
+ sharedDenialReason(meta, "media_proxy", "github", "oauth_token"),
576
+ );
558
577
  });
559
578
 
560
579
  test("returns empty injection templates when credential has none", async () => {
@@ -146,6 +146,7 @@ mock.module("../tools/credentials/broker.js", () => ({
146
146
  }));
147
147
 
148
148
  import { persistPromptedCredential } from "../credential-execution/prompted-credential.js";
149
+ import { initializeDb } from "../persistence/db-init.js";
149
150
  import {
150
151
  forChatMintsSince,
151
152
  resetForChatMintRegistryForTest,
@@ -158,6 +159,12 @@ import {
158
159
  } from "../runtime/reveal-success-registry.js";
159
160
  import { ROUTES } from "../runtime/routes/credential-routes.js";
160
161
 
162
+ // A delete looks up the provider connections that resolve their auth through
163
+ // the credential, so the inference schema has to exist. No connection is
164
+ // created here: the refusal is covered by
165
+ // runtime/routes/__tests__/credential-delete-in-use.test.ts.
166
+ await initializeDb();
167
+
161
168
  const setRoute = ROUTES.find((r) => r.operationId === "credentials_set");
162
169
  const listRoute = ROUTES.find((r) => r.operationId === "credentials_list");
163
170
  const deleteRoute = ROUTES.find((r) => r.operationId === "credentials_delete");
@@ -173,7 +180,11 @@ type ListResponse = {
173
180
  }>;
174
181
  managedCredentials: unknown[];
175
182
  };
176
- type DeleteResponse = { service: string; field: string };
183
+ type DeleteResponse = {
184
+ service: string;
185
+ field: string;
186
+ affectedConnections: string[];
187
+ };
177
188
 
178
189
  const SECRET_VALUE = "super-secret-token-value";
179
190
 
@@ -786,7 +797,11 @@ describe("credentials routes", () => {
786
797
  })) as DeleteResponse;
787
798
 
788
799
  // THEN the response echoes the identifiers
789
- expect(result).toEqual({ service: "vercel", field: "api_token" });
800
+ expect(result).toEqual({
801
+ service: "vercel",
802
+ field: "api_token",
803
+ affectedConnections: [],
804
+ });
790
805
 
791
806
  // AND the secret and metadata are gone
792
807
  expect(secureStore.has("vercel:api_token")).toBe(false);
@@ -811,7 +826,11 @@ describe("credentials routes", () => {
811
826
  })) as DeleteResponse;
812
827
 
813
828
  // THEN the response echoes the identifiers
814
- expect(result).toEqual({ service: "slack_channel", field: "user_token" });
829
+ expect(result).toEqual({
830
+ service: "slack_channel",
831
+ field: "user_token",
832
+ affectedConnections: [],
833
+ });
815
834
 
816
835
  // AND only the user_token is removed; bot + app tokens remain
817
836
  expect(secureStore.has("slack_channel:user_token")).toBe(false);
@@ -237,7 +237,8 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
237
237
  "tools/network/web-fetch.ts", // Firecrawl /scrape BYOK fetch provider API key lookup (firecrawl provider key)
238
238
  "workspace/default-provider-ensure.ts", // legacy anthropic echo disambiguation (vault key presence check)
239
239
  "providers/inference/connection-availability.ts", // shared (provider, connection) availability status (credential presence check only; value never leaves the helper)
240
- "plugin-api/resolve-credential.ts", // plugin-facing resolveCredential — reveal-equivalent plaintext read, scoped to the in-context plugin's own field
240
+ "plugin-api/resolve-credential.ts", // plugin-facing resolveCredential: reveal-equivalent plaintext read, scoped to the in-context plugin's own service
241
+ "tools/credentials/store.ts", // shared credential write path (setSecureKeyAsync only; no reads) behind credentials/set and plugin-facing storeCredential
241
242
  ]);
242
243
 
243
244
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -104,6 +104,60 @@ describe("daemon credential client", () => {
104
104
  expect(result.error).toBe("Credential store is unreachable");
105
105
  });
106
106
 
107
+ test("refuses to delete around the in-use guard when the daemon is down", async () => {
108
+ /**
109
+ * The guard runs daemon-side, so a stopped daemon must not turn into an
110
+ * unchecked direct delete.
111
+ */
112
+
113
+ // GIVEN a stored credential and no reachable daemon
114
+ _ipcResponse = {
115
+ ok: false,
116
+ error: "Could not connect to assistant daemon. Is it running?",
117
+ };
118
+ await setSecureKeyViaDaemon("api_key", "guarded-provider", "keep-me");
119
+
120
+ // WHEN the delete is attempted without force
121
+ const result = await deleteSecureKeyViaDaemon(
122
+ "api_key",
123
+ "guarded-provider",
124
+ );
125
+
126
+ // THEN it is refused and the credential survives
127
+ expect(result.result).toBe("error");
128
+ expect(result.code).toBe("IN_USE_CHECK_UNAVAILABLE");
129
+ expect(result.error).toContain("--force");
130
+ expect(
131
+ await getSecureKeyAsync(credentialKey("guarded-provider", "api_key")),
132
+ ).toBe("keep-me");
133
+ });
134
+
135
+ test("deletes directly when the caller forces past the unavailable guard", async () => {
136
+ /**
137
+ * An explicit --force still works with the daemon down.
138
+ */
139
+
140
+ // GIVEN a stored credential and no reachable daemon
141
+ _ipcResponse = {
142
+ ok: false,
143
+ error: "Could not connect to assistant daemon. Is it running?",
144
+ };
145
+ await setSecureKeyViaDaemon("api_key", "forced-provider", "drop-me");
146
+
147
+ // WHEN the delete is forced
148
+ const result = await deleteSecureKeyViaDaemon(
149
+ "api_key",
150
+ "forced-provider",
151
+ true,
152
+ );
153
+
154
+ // THEN the credential is gone from secure storage
155
+ expect(result.result).toBe("deleted");
156
+ expect(
157
+ await getSecureKeyAsync(credentialKey("forced-provider", "api_key")),
158
+ ).toBeUndefined();
159
+ });
160
+
107
161
  test("returns not-found for 404 errors", async () => {
108
162
  _ipcResponse = {
109
163
  ok: false,
@@ -167,5 +221,39 @@ describe("daemon credential client", () => {
167
221
  body: { type: "credential", name: "github-app:pem" },
168
222
  });
169
223
  });
224
+
225
+ test("delete carries the caller's force choice past the in-use guard", async () => {
226
+ // GIVEN a daemon that accepts the delete
227
+ _ipcResponse = { ok: true, result: { success: true } };
228
+ _lastIpcCall = null;
229
+
230
+ // WHEN the caller forces the delete
231
+ await deleteSecureKeyViaDaemon("api_key", "agentrouter", true);
232
+
233
+ // THEN `force` rides along in the request body
234
+ const captured = _lastIpcCall as {
235
+ method: string;
236
+ params: unknown;
237
+ } | null;
238
+ expect(captured?.params).toEqual({
239
+ body: { type: "api_key", name: "agentrouter", force: true },
240
+ });
241
+ });
242
+
243
+ test("delete surfaces the in-use refusal from the daemon", async () => {
244
+ // GIVEN a credential an LLM provider connection resolves its auth through
245
+ _ipcResponse = {
246
+ ok: false,
247
+ error:
248
+ 'Credential credential/agentrouter/api_key is in use by connection "agentrouter". It will stop working without it. Delete it anyway to continue.',
249
+ };
250
+
251
+ // WHEN the delete is attempted without force
252
+ const result = await deleteSecureKeyViaDaemon("api_key", "agentrouter");
253
+
254
+ // THEN the refusal reaches the caller naming the dependent connection
255
+ expect(result.result).toBe("error");
256
+ expect(result.error).toContain('in use by connection "agentrouter"');
257
+ });
170
258
  });
171
259
  });
@@ -1,7 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { getAllDefaultPlugins } from "../plugins/defaults/index.js";
4
- import { getAllDefaultPluginNames } from "../plugins/defaults/main.js";
4
+ import {
5
+ getAllDefaultPluginNames,
6
+ isFirstPartyDefaultPlugin,
7
+ } from "../plugins/defaults/main.js";
5
8
 
6
9
  /**
7
10
  * Guard: the filesystem-backed default-plugin registry (`defaults/main.ts`)
@@ -25,4 +28,12 @@ describe("default plugin names", () => {
25
28
  expect(name).toMatch(/^default-[a-z0-9-]+$/);
26
29
  }
27
30
  });
31
+
32
+ test("isFirstPartyDefaultPlugin matches the filesystem registry and rejects other names", () => {
33
+ for (const name of getAllDefaultPluginNames()) {
34
+ expect(isFirstPartyDefaultPlugin(name)).toBe(true);
35
+ }
36
+ expect(isFirstPartyDefaultPlugin("default-a")).toBe(false);
37
+ expect(isFirstPartyDefaultPlugin("uplug-a")).toBe(false);
38
+ });
28
39
  });
@@ -180,6 +180,8 @@ function readPersistedSlackRows(): Array<{
180
180
  provenanceSourceChannel: string | undefined;
181
181
  provenanceGuardianExternalUserId: string | undefined;
182
182
  provenanceRequesterIdentifier: string | undefined;
183
+ sentAt: number | undefined;
184
+ createdAt: number;
183
185
  }> {
184
186
  const db = getDb();
185
187
  return db
@@ -187,6 +189,7 @@ function readPersistedSlackRows(): Array<{
187
189
  role: messages.role,
188
190
  content: messages.content,
189
191
  metadata: messages.metadata,
192
+ createdAt: messages.createdAt,
190
193
  })
191
194
  .from(messages)
192
195
  .all()
@@ -231,6 +234,9 @@ function readPersistedSlackRows(): Array<{
231
234
  typeof envelope.provenanceRequesterIdentifier === "string"
232
235
  ? envelope.provenanceRequesterIdentifier
233
236
  : undefined,
237
+ sentAt:
238
+ typeof envelope.sentAt === "number" ? envelope.sentAt : undefined,
239
+ createdAt: row.createdAt,
234
240
  };
235
241
  });
236
242
  }
@@ -587,4 +593,61 @@ describe("PR 23 — Slack DM cold-start backfill", () => {
587
593
  const texts = rows.map((r) => r.content).sort();
588
594
  expect(texts).toEqual(["older A", "older B", "older D"]);
589
595
  });
596
+
597
+ test("backfilled rows carry the original send time as sentAt", async () => {
598
+ // History serialization prefers `sentAt` for the display timestamp, so a
599
+ // row without it reads as having arrived when the import ran.
600
+ const sentAt = Date.UTC(2026, 6, 8, 9, 15);
601
+ backfillDmMock.mockImplementation(async () => [
602
+ makeBackfilledMessage({
603
+ id: String(sentAt / 1000),
604
+ text: "six weeks old",
605
+ timestamp: sentAt,
606
+ }),
607
+ ]);
608
+
609
+ await handleChannelInbound(
610
+ buildDmRequest("live new DM"),
611
+ noopProcessMessage,
612
+ TEST_BEARER_TOKEN,
613
+ );
614
+
615
+ const [row] = readPersistedSlackRows();
616
+ expect(row).toBeDefined();
617
+ expect(row.sentAt).toBe(sentAt);
618
+ // The row is still written now; only the reported send time is historical.
619
+ // Asserting the gap is what proves `sentAt` is not just echoing createdAt.
620
+ expect(row.createdAt).toBeGreaterThan(sentAt);
621
+ });
622
+
623
+ test("backfill refuses history rows carrying a secret, keeping the rest", async () => {
624
+ const leakedKey = "AKIA3H7QWERTY9MNBVC2";
625
+ backfillDmMock.mockImplementation(async () => [
626
+ makeBackfilledMessage({
627
+ id: "1700000000.000001",
628
+ text: "ordinary older chatter",
629
+ }),
630
+ makeBackfilledMessage({
631
+ id: "1700000000.000002",
632
+ text: `here is the access key ${leakedKey} for the deploy`,
633
+ }),
634
+ ]);
635
+
636
+ await handleChannelInbound(
637
+ buildDmRequest("live new DM"),
638
+ noopProcessMessage,
639
+ TEST_BEARER_TOKEN,
640
+ );
641
+
642
+ const rows = readPersistedSlackRows();
643
+ expect(rows.map((r) => r.content)).toEqual(["ordinary older chatter"]);
644
+ // `rawContent`, not `content`: the latter is unwrapped by the test
645
+ // helper, so only the raw column proves what reached storage.
646
+ for (const row of rows) {
647
+ expect(row.rawContent).not.toContain(leakedKey);
648
+ }
649
+ expect(
650
+ rows.some((r) => r.slackMeta?.channelTs === "1700000000.000002"),
651
+ ).toBe(false);
652
+ });
590
653
  });
@@ -65,9 +65,7 @@ mock.module("../permissions/checker.js", () => ({
65
65
  isDynamicSkillLoadInvocation: () => dynamicSkillLoad,
66
66
  classifyRisk: async () => ({ level: "medium" }),
67
67
  check: async () => ({ decision: checkDecision, reason: "medium risk" }),
68
- generateAllowlistOptions: () => [],
69
68
  generateScopeOptions: () => [],
70
- getCachedAssessment: () => undefined,
71
69
  }));
72
70
 
73
71
  mock.module("../telemetry/tool-usage-store.js", () => ({
@@ -171,7 +171,100 @@ describe("Slack edit propagation", () => {
171
171
  expect(slackMeta!.editedAt!).toBeGreaterThanOrEqual(t0);
172
172
  });
173
173
 
174
- test("threaded Slack edits use the threaded conversation key and preserve thread metadata", async () => {
174
+ // Times out on the default budget: an unresolvable target pays the full
175
+ // `EDIT_LOOKUP_RETRIES` window before it is dropped.
176
+ test("an edit of a message the assistant never stored creates no conversation", async () => {
177
+ // Keying a conversation off the edit's own address made every edit of an
178
+ // unseen message mint one, permanently titled with the loading placeholder.
179
+ const db = getDb();
180
+ const countRows = (table: string): number =>
181
+ (
182
+ db.$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
183
+ n: number;
184
+ }
185
+ ).n;
186
+
187
+ const resp = (await handleEditIntercept({
188
+ sourceChannel: "slack",
189
+ conversationExternalId: "C0NEVERSEEN",
190
+ externalMessageId: nextEditEventId(),
191
+ sourceMessageId: "1999.0001",
192
+ assistantId: "self",
193
+ content: "edited text",
194
+ })) as Record<string, unknown>;
195
+
196
+ expect(resp.accepted).toBe(true);
197
+ expect(resp.edited).toBe(false);
198
+ expect(countRows("conversations")).toBe(0);
199
+ expect(countRows("channel_inbound_events")).toBe(0);
200
+ expect(countRows("messages")).toBe(0);
201
+ }, 30_000);
202
+
203
+ test("an edit records against the edited message's conversation, not a new one", async () => {
204
+ const seeded = await seedSlackMessage({
205
+ conversationExternalId: "C0123CHANNEL",
206
+ channelTs: "1234.5678",
207
+ initialContent: "original text",
208
+ });
209
+ const db = getDb();
210
+ const conversationsBefore = (
211
+ db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
212
+ n: number;
213
+ }
214
+ ).n;
215
+
216
+ await handleEditIntercept({
217
+ sourceChannel: "slack",
218
+ conversationExternalId: seeded.conversationExternalId,
219
+ externalMessageId: nextEditEventId(),
220
+ sourceMessageId: seeded.channelTs,
221
+ assistantId: "self",
222
+ content: "new text",
223
+ });
224
+
225
+ const conversationsAfter = (
226
+ db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
227
+ n: number;
228
+ }
229
+ ).n;
230
+ expect(conversationsAfter).toBe(conversationsBefore);
231
+
232
+ const editEvents = db.$client
233
+ .prepare(
234
+ `SELECT conversation_id AS conversationId FROM channel_inbound_events
235
+ WHERE external_message_id LIKE 'edit-event-%'`,
236
+ )
237
+ .all() as Array<{ conversationId: string }>;
238
+ expect(editEvents).toHaveLength(1);
239
+ expect(editEvents[0].conversationId).toBe(seeded.conversationId);
240
+ });
241
+
242
+ test("a redelivered edit is answered without repeating the lookup window", async () => {
243
+ const seeded = await seedSlackMessage({
244
+ conversationExternalId: "C0123CHANNEL",
245
+ channelTs: "1234.5678",
246
+ initialContent: "original text",
247
+ });
248
+ const editEventId = nextEditEventId();
249
+ const send = () =>
250
+ handleEditIntercept({
251
+ sourceChannel: "slack",
252
+ conversationExternalId: seeded.conversationExternalId,
253
+ externalMessageId: editEventId,
254
+ sourceMessageId: seeded.channelTs,
255
+ assistantId: "self",
256
+ content: "new text",
257
+ }) as Promise<Record<string, unknown>>;
258
+
259
+ const first = await send();
260
+ const second = await send();
261
+
262
+ expect(first.duplicate).toBe(false);
263
+ expect(second.duplicate).toBe(true);
264
+ expect(second.eventId).toBe(first.eventId);
265
+ });
266
+
267
+ test("a threaded edit stays in the edited message's conversation and preserves thread metadata", async () => {
175
268
  const conversationExternalId = "C0123CHANNEL";
176
269
  const threadTs = "1234.0000";
177
270
  const seeded = await seedSlackMessage({
@@ -179,6 +272,14 @@ describe("Slack edit propagation", () => {
179
272
  channelTs: "1234.5678",
180
273
  initialContent: "original text",
181
274
  });
275
+ const db = getDb();
276
+ const conversationCount = (): number =>
277
+ (
278
+ db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
279
+ n: number;
280
+ }
281
+ ).n;
282
+ const before = conversationCount();
182
283
 
183
284
  const resp = await handleEditIntercept({
184
285
  sourceChannel: "slack",
@@ -191,10 +292,12 @@ describe("Slack edit propagation", () => {
191
292
  });
192
293
 
193
294
  expect((resp as Record<string, unknown>).accepted).toBe(true);
295
+
296
+ // The thread id names where the reply goes, not a conversation of the
297
+ // edit's own. Keying one off it is what minted a conversation per edit.
194
298
  const threadedKey = `asst:self:slack:${conversationExternalId}:thread:${threadTs}`;
195
- const editConversation = getConversationByKey(threadedKey);
196
- expect(editConversation).not.toBeNull();
197
- expect(editConversation!.conversationId).not.toBe(seeded.conversationId);
299
+ expect(getConversationByKey(threadedKey)).toBeNull();
300
+ expect(conversationCount()).toBe(before);
198
301
 
199
302
  const after = readMessageRow(seeded.messageId);
200
303
  const outer = JSON.parse(after.metadata!) as Record<string, unknown>;
@@ -54,6 +54,34 @@ describe("events client registration", () => {
54
54
  ac.abort();
55
55
  });
56
56
 
57
+ test("registers Windows with only its implemented host capabilities", () => {
58
+ const ac = new AbortController();
59
+ const hub = new AssistantEventHub();
60
+
61
+ handleSubscribeAssistantEvents(
62
+ {
63
+ headers: {
64
+ "x-vellum-client-id": "test-windows-001",
65
+ "x-vellum-interface-id": "windows",
66
+ },
67
+ abortSignal: ac.signal,
68
+ },
69
+ { hub },
70
+ );
71
+
72
+ const entry = hub.getClientById("test-windows-001");
73
+ expect(entry?.interfaceId).toBe("windows");
74
+ expect(entry?.capabilities).toEqual([
75
+ "host_bash",
76
+ "host_file",
77
+ "host_cu",
78
+ "host_browser",
79
+ "host_ui_snapshot",
80
+ ]);
81
+
82
+ ac.abort();
83
+ });
84
+
57
85
  test("skips registration when no headers are provided (backwards compat)", () => {
58
86
  const ac = new AbortController();
59
87
  const hub = new AssistantEventHub();
@@ -76,8 +76,10 @@ beforeEach(() => {
76
76
  enqueueCalls.length = 0;
77
77
  enqueueThrows = false;
78
78
  // Reset to a stable tmp path so the sandbox tests (which don't use pkb/)
79
- // deterministically land outside any configured PKB root.
80
- process.env.VELLUM_WORKSPACE_DIR = tmpdir();
79
+ // deterministically land outside any configured PKB root. A subdirectory
80
+ // rather than tmpdir() itself, so vellumRoot() (the parent) also stays
81
+ // inside the temp directory for the live-workspace guard.
82
+ process.env.VELLUM_WORKSPACE_DIR = join(tmpdir(), "file-write-tool-ws");
81
83
  });
82
84
 
83
85
  afterEach(() => {