@vellumai/assistant 0.11.4 → 0.11.5-staging.2

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 (367) 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__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -47,6 +47,14 @@ const getConversationMock = mock((id: string) => {
47
47
  return mockExistingConversations[id] ?? null;
48
48
  });
49
49
 
50
+ const messagesInvalidated: string[] = [];
51
+
52
+ mock.module("../runtime/sync/resource-sync-events.js", () => ({
53
+ publishConversationMessagesChanged: (conversationId: string) => {
54
+ messagesInvalidated.push(conversationId);
55
+ },
56
+ }));
57
+
50
58
  mock.module("../persistence/conversation-crud.js", () => ({
51
59
  setConversationProcessingStartedAt: () => {},
52
60
  isConversationProcessing: () => false,
@@ -144,6 +152,7 @@ describe("pairDeliveryWithConversation", () => {
144
152
  beforeEach(() => {
145
153
  createConversationMock.mockClear();
146
154
  addMessageMock.mockClear();
155
+ messagesInvalidated.length = 0;
147
156
  getConversationMock.mockClear();
148
157
  getBindingByChannelChatMock.mockClear();
149
158
  upsertOutboundBindingMock.mockClear();
@@ -870,6 +879,183 @@ describe("pairDeliveryWithConversation", () => {
870
879
  expect(addMessageMock).not.toHaveBeenCalled();
871
880
  });
872
881
 
882
+ // ── Passive default: body appended to the producing conversation ───
883
+ //
884
+ // The home feed points "Go to Conversation" at `sourceContextId`, so the
885
+ // body has to land there for that button to open something that contains
886
+ // the notification the user tapped.
887
+
888
+ test("passive vellum signal appends the body to the producing conversation", async () => {
889
+ mockExistingConversations["conv-producer"] = {
890
+ id: "conv-producer",
891
+ source: "schedule",
892
+ title: "Daily Briefing Delivery",
893
+ };
894
+ const signal = makeSignal({
895
+ requiresConversation: undefined,
896
+ sourceContextId: "conv-producer",
897
+ });
898
+ const copy = makeCopy({ body: "Daily Briefing for Tuesday." });
899
+
900
+ const result = await pairDeliveryWithConversation(
901
+ signal,
902
+ "vellum" as NotificationChannel,
903
+ copy,
904
+ );
905
+
906
+ expect(result.conversationId).toBe("conv-producer");
907
+ expect(result.messageId).toBe("msg-001");
908
+ expect(result.createdNewConversation).toBe(false);
909
+ expect(result.conversationFallbackUsed).toBe(false);
910
+ // Appending must never mint a sidebar entry.
911
+ expect(createConversationMock).not.toHaveBeenCalled();
912
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
913
+ const [conversationId, role] = addMessageMock.mock.calls[0]!;
914
+ expect(conversationId).toBe("conv-producer");
915
+ expect(role).toBe("assistant");
916
+ // `addMessage` publishes the metadata tag alone, so a client holding this
917
+ // conversation open needs the messages tag to refetch the transcript.
918
+ expect(messagesInvalidated).toContain("conv-producer");
919
+ });
920
+
921
+ test("appends for a foreground producer the home feed will not mirror", async () => {
922
+ // `resolveHomeFeedMirror` declines a signal that is neither
923
+ // `assistant_tool`, nor flagged async-background, nor produced by a
924
+ // background-typed conversation, so no card is written for this one. The
925
+ // append is deliberately not gated on that: the vellum banner still
926
+ // deep-links to this conversation, and the client suppresses the banner
927
+ // entirely when the user is already viewing it, leaving the transcript as
928
+ // the only place the notification can land.
929
+ mockExistingConversations["conv-foreground"] = {
930
+ id: "conv-foreground",
931
+ source: "user",
932
+ title: "An ordinary chat",
933
+ };
934
+ const signal = makeSignal({
935
+ requiresConversation: undefined,
936
+ sourceChannel: "plugin",
937
+ sourceContextId: "conv-foreground",
938
+ attentionHints: {
939
+ requiresAction: false,
940
+ urgency: "medium",
941
+ isAsyncBackground: false,
942
+ visibleInSourceNow: false,
943
+ },
944
+ });
945
+ const copy = makeCopy({ body: "Something you asked to watch changed." });
946
+
947
+ const result = await pairDeliveryWithConversation(
948
+ signal,
949
+ "vellum" as NotificationChannel,
950
+ copy,
951
+ );
952
+
953
+ expect(result.conversationId).toBe("conv-foreground");
954
+ expect(createConversationMock).not.toHaveBeenCalled();
955
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
956
+ expect(messagesInvalidated).toContain("conv-foreground");
957
+ });
958
+
959
+ test("appended notification body skips indexing", async () => {
960
+ mockExistingConversations["conv-producer"] = {
961
+ id: "conv-producer",
962
+ source: "schedule",
963
+ title: null,
964
+ };
965
+ const signal = makeSignal({
966
+ requiresConversation: undefined,
967
+ sourceContextId: "conv-producer",
968
+ });
969
+
970
+ await pairDeliveryWithConversation(
971
+ signal,
972
+ "vellum" as NotificationChannel,
973
+ makeCopy(),
974
+ );
975
+
976
+ const options = addMessageMock.mock.calls[0]![3] as {
977
+ skipIndexing?: boolean;
978
+ };
979
+ expect(options.skipIndexing).toBe(true);
980
+ });
981
+
982
+ test("passive vellum signal appends regardless of the producing conversation's source", async () => {
983
+ // The reuse_existing branch requires a source match; appending to the
984
+ // producing conversation does not, since the row already exists and the
985
+ // feed links to it either way.
986
+ mockExistingConversations["conv-producer"] = {
987
+ id: "conv-producer",
988
+ source: "user",
989
+ title: null,
990
+ };
991
+ const signal = makeSignal({
992
+ requiresConversation: undefined,
993
+ sourceContextId: "conv-producer",
994
+ conversationMetadata: { source: "notification" },
995
+ });
996
+
997
+ const result = await pairDeliveryWithConversation(
998
+ signal,
999
+ "vellum" as NotificationChannel,
1000
+ makeCopy(),
1001
+ );
1002
+
1003
+ expect(result.conversationId).toBe("conv-producer");
1004
+ expect(createConversationMock).not.toHaveBeenCalled();
1005
+ });
1006
+
1007
+ test("passive vellum signal with a sentinel sourceContextId appends nothing", async () => {
1008
+ // Producers pass job ids, call session ids and `access-req-*` strings
1009
+ // here. Those resolve to no row, which matches the feed hiding the button.
1010
+ const signal = makeSignal({
1011
+ requiresConversation: undefined,
1012
+ sourceContextId: "job-1234",
1013
+ });
1014
+
1015
+ const result = await pairDeliveryWithConversation(
1016
+ signal,
1017
+ "vellum" as NotificationChannel,
1018
+ makeCopy(),
1019
+ );
1020
+
1021
+ expect(result.conversationId).toBeNull();
1022
+ expect(result.messageId).toBeNull();
1023
+ expect(createConversationMock).not.toHaveBeenCalled();
1024
+ expect(addMessageMock).not.toHaveBeenCalled();
1025
+ });
1026
+
1027
+ test("passive vellum signal appends to the producer, not the reuse_existing target", async () => {
1028
+ mockExistingConversations["conv-producer"] = {
1029
+ id: "conv-producer",
1030
+ source: "schedule",
1031
+ title: null,
1032
+ };
1033
+ mockExistingConversations["conv-hinted"] = {
1034
+ id: "conv-hinted",
1035
+ source: "notification",
1036
+ title: null,
1037
+ };
1038
+ const signal = makeSignal({
1039
+ requiresConversation: undefined,
1040
+ sourceContextId: "conv-producer",
1041
+ });
1042
+ const conversationAction: ConversationAction = {
1043
+ action: "reuse_existing",
1044
+ conversationId: "conv-hinted",
1045
+ };
1046
+
1047
+ const result = await pairDeliveryWithConversation(
1048
+ signal,
1049
+ "vellum" as NotificationChannel,
1050
+ makeCopy(),
1051
+ { conversationAction },
1052
+ );
1053
+
1054
+ expect(result.conversationId).toBe("conv-producer");
1055
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
1056
+ expect(addMessageMock.mock.calls[0]![0]).toBe("conv-producer");
1057
+ });
1058
+
873
1059
  // ── conversationMetadata.conversationType override ─────────────────
874
1060
 
875
1061
  test("uses conversationMetadata.conversationType when set, overriding channel strategy", async () => {
@@ -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", () => ({