@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
@@ -691,17 +691,16 @@ async function handleRetryLastAssistantTurn({
691
691
  const conversation = await getOrCreateConversationInstance(conversationId);
692
692
  touchConversation(conversationId);
693
693
 
694
- // Bind the requesting actor's trust before the turn. A freshly hydrated
695
- // conversation has no trust context, and `loadFromDb` under an unset
696
- // context filters guardian-provenance history — the re-run would see an
697
- // empty transcript.
694
+ // Resolved before the busy gate so the awaits stay outside the
695
+ // check-then-claim window below, but bound only once the claim succeeds: a
696
+ // retry that loses the race is rejected, and a rejected request must not
697
+ // repoint the conversation's actor at its requester.
698
698
  const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
699
- conversation.setTrustContext(
700
- await resolveVellumActorTrustContext(actorPrincipalId, {
701
- healResetDrift: true,
702
- }),
699
+ const retryTrustContext = await resolveVellumActorTrustContext(
700
+ actorPrincipalId,
701
+ { healResetDrift: true },
703
702
  );
704
- conversation.currentTurnSourceActorPrincipalId =
703
+ const retrySourceActorPrincipalId =
705
704
  await resolveActorPrincipalIdForLocalGuardian(actorPrincipalId);
706
705
 
707
706
  // Synchronous check-then-claim (no await between them) so a concurrent
@@ -714,6 +713,13 @@ async function handleRetryLastAssistantTurn({
714
713
  }
715
714
  conversation.setProcessing(true);
716
715
 
716
+ // Bind the requesting actor's trust for the turn we just claimed. A freshly
717
+ // hydrated conversation has no trust context, and `loadFromDb` under an
718
+ // unset context filters guardian-provenance history — the re-run would see
719
+ // an empty transcript.
720
+ conversation.setTrustContext(retryTrustContext);
721
+ conversation.currentTurnSourceActorPrincipalId = retrySourceActorPrincipalId;
722
+
717
723
  const requestId = uuidv7();
718
724
  const abortController = new AbortController();
719
725
  let discarded: ReturnType<typeof discardLastAssistantDisplayTurn>;
@@ -1236,12 +1236,10 @@ export async function handleListMessages({
1236
1236
 
1237
1237
  const alignedContentOrder = aligned.rewriteContentOrder(contentOrder);
1238
1238
 
1239
- // Use sentAt (actual event time) for the display timestamp when available,
1240
- // falling back to createdAt (persistence time). Clients use this display
1241
- // timestamp as their pagination cursor after memory-pressure trimming,
1242
- // while server-side pagination filters on createdAt. The mismatch is
1243
- // benign: it may return slightly extra data on a page boundary but never
1244
- // loses messages.
1239
+ // Use sentAt (actual event time) for the display timestamp when
1240
+ // available, falling back to createdAt (persistence time). Pagination
1241
+ // is unaffected: the cursor is `oldestTimestamp`, built from
1242
+ // `createdAt` on both ends.
1245
1243
  const displayTimestamp = m.sentAt ?? m.createdAt;
1246
1244
  return {
1247
1245
  id: m.id ?? "",
@@ -1849,6 +1847,11 @@ export async function handleSendMessage(
1849
1847
 
1850
1848
  // Resolve guardian context from the AuthContext's actorPrincipalId via the
1851
1849
  // gateway guardian binding: a vellum principal is the guardian or nobody.
1850
+ //
1851
+ // Resolved into a local; the conversation's slot is stamped only where this
1852
+ // request commits to running a turn, since that write is what supplies the
1853
+ // acting actor for a run and must not fire for a send that merely queues.
1854
+ let resolvedTrustCtx: TrustContext;
1852
1855
  if (actorPrincipalId) {
1853
1856
  // Dev bypass (HTTP auth disabled): the synthetic "dev-bypass" principal
1854
1857
  // won't match any guardian binding. Resolve the real guardian principal and
@@ -1874,7 +1877,7 @@ export async function handleSendMessage(
1874
1877
  trustCtx = healed;
1875
1878
  }
1876
1879
  }
1877
- conversation.setTrustContext(trustCtx);
1880
+ resolvedTrustCtx = trustCtx;
1878
1881
  } else {
1879
1882
  let trustCtx = withSourceChannel(
1880
1883
  sourceChannel,
@@ -1907,22 +1910,19 @@ export async function handleSendMessage(
1907
1910
  );
1908
1911
  }
1909
1912
  }
1910
- conversation.setTrustContext(trustCtx);
1913
+ resolvedTrustCtx = trustCtx;
1911
1914
  }
1912
1915
  } else {
1913
1916
  // Service principals (svc_gateway) or tokens without an actor ID
1914
1917
  // get a minimal guardian context so downstream code has something.
1915
- conversation.setTrustContext({ trustClass: "guardian", sourceChannel });
1918
+ resolvedTrustCtx = { trustClass: "guardian", sourceChannel };
1916
1919
  }
1917
1920
 
1918
- // The trust this request's turn runs under, captured here rather than read
1919
- // back at the agent loop below. Every branch above has just written it and
1920
- // nothing awaits in between, so this is the resolved sender. Between here
1921
- // and the loop the conversation slot is writable by paths that do not own
1922
- // this turn (channel ingress for another actor, live-voice hydration,
1923
- // pointer elevation, the voice bridge), and the loop would otherwise
1924
- // re-read it and run this turn as whoever wrote last.
1925
- const turnTrustContext = conversation.trustContext;
1921
+ // The trust this request's turn runs under: the sender resolved above,
1922
+ // never a read of the shared slot. Between here and the loop that slot is
1923
+ // writable by paths that do not own this turn (channel ingress for another
1924
+ // actor, live-voice hydration, pointer elevation, the voice bridge).
1925
+ const turnTrustContext = resolvedTrustCtx;
1926
1926
 
1927
1927
  const isInteractive = isInteractiveInterface(sourceInterface);
1928
1928
  // Translate the dev-bypass actor principal to the real guardian principal
@@ -2053,12 +2053,15 @@ export async function handleSendMessage(
2053
2053
  requestId: uuidv7(),
2054
2054
  metadata: greetingMeta,
2055
2055
  clientMessageId,
2056
+ // This path answers and returns without starting a turn, so it never
2057
+ // reaches the stamp below; name the sender on the row directly.
2058
+ trustContext: resolvedTrustCtx,
2056
2059
  ...(clientOs ? { requestClientOs: clientOs } : {}),
2057
2060
  });
2058
2061
 
2059
2062
  const conversationId = mapping.conversationId;
2060
2063
  const channelMeta = buildChannelMetadata(sourceChannel, sourceInterface, {
2061
- trustContext: conversation.trustContext,
2064
+ trustContext: resolvedTrustCtx,
2062
2065
  });
2063
2066
 
2064
2067
  const assistantMsg = createAssistantMessage(cannedGreeting);
@@ -2165,10 +2168,9 @@ export async function handleSendMessage(
2165
2168
 
2166
2169
  // Resolve the verified actor's external user ID and principal for inline
2167
2170
  // approval routing from the conversation's guardian context.
2168
- const verifiedActorExternalUserId =
2169
- conversation.trustContext?.guardianExternalUserId;
2171
+ const verifiedActorExternalUserId = resolvedTrustCtx.guardianExternalUserId;
2170
2172
  const verifiedActorPrincipalId =
2171
- conversation.trustContext?.guardianPrincipalId ?? undefined;
2173
+ resolvedTrustCtx.guardianPrincipalId ?? undefined;
2172
2174
 
2173
2175
  // Try to consume the message as a guardian approval/rejection reply.
2174
2176
  // On failure, degrade to the existing queue/auto-deny path rather than
@@ -2242,6 +2244,10 @@ export async function handleSendMessage(
2242
2244
  sourceActorPrincipalId,
2243
2245
  transport,
2244
2246
  clientMessageId,
2247
+ // The sender's own trust, so the drain runs this message as the actor
2248
+ // who sent it rather than as whoever the slot happens to hold when the
2249
+ // queue is worked.
2250
+ trustContext: resolvedTrustCtx,
2245
2251
  });
2246
2252
  if (enqueueResult.rejected) {
2247
2253
  return new RouteResponse(
@@ -2340,7 +2346,15 @@ export async function handleSendMessage(
2340
2346
  // matching in-memory pending interaction (e.g. prompter timeouts).
2341
2347
  await expireOrphanedGuardianRequests(mapping.conversationId);
2342
2348
 
2343
- // Conversation is idle — persist and fire agent loop immediately
2349
+ // Conversation is idle — persist and fire agent loop immediately.
2350
+ //
2351
+ // Stamping the sender here rather than at resolution is what keeps the two
2352
+ // in step: the slot hydrates and scopes the turn started just below
2353
+ // (`ensureActorScopedHistory`, persisted provenance, the loop's own trust),
2354
+ // so it must name whoever this request is about to run as. A request that
2355
+ // queues instead returns above without stamping — it is not starting a run,
2356
+ // and its actor rides the queue item to the drain.
2357
+ conversation.setTrustContext(resolvedTrustCtx);
2344
2358
  conversation.setTurnChannelContext({
2345
2359
  userMessageChannel: sourceChannel,
2346
2360
  assistantMessageChannel: sourceChannel,
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared in-use guard for the credential-delete surfaces
3
+ * (`POST /v1/credentials/delete` and `DELETE /v1/secrets`).
4
+ *
5
+ * A credential removed while an LLM connection resolves its auth through it
6
+ * takes that connection offline, so such a delete is refused until the caller
7
+ * confirms it with `force`. Both surfaces run the same guard, so a protection on
8
+ * only one of them is one a client can route around.
9
+ */
10
+
11
+ import { findConnectionsUsingCredential } from "../../providers/inference/credential-usage.js";
12
+ import { clearConnectionProviderCache } from "../../providers/registry.js";
13
+ import { getLogger } from "../../util/logger.js";
14
+ import { RouteError } from "./errors.js";
15
+
16
+ const log = getLogger("runtime/routes/credential-in-use");
17
+
18
+ /**
19
+ * A credential is still referenced by at least one provider connection.
20
+ *
21
+ * Carries the dependent connection names in both the message and
22
+ * `details.connections`, so a client can name them in its confirmation
23
+ * prompt, and a distinct `code` so it can tell this refusal apart from an
24
+ * ordinary bad request and offer the forced retry.
25
+ */
26
+ export class CredentialInUseError extends RouteError {
27
+ constructor(
28
+ readonly connections: string[],
29
+ message: string,
30
+ ) {
31
+ super(message, "CREDENTIAL_IN_USE", 400, { connections });
32
+ this.name = "CredentialInUseError";
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Refuse to delete `credentialAccount` while provider connections resolve
38
+ * their auth through it, unless `force` is set. Returns the affected
39
+ * connection names so the caller can invalidate what depended on them.
40
+ *
41
+ * The credential itself is never logged beyond its `credential/{service}/
42
+ * {field}` account name, which holds no secret material.
43
+ */
44
+ export function assertCredentialNotInUse(
45
+ credentialAccount: string,
46
+ force: boolean,
47
+ ): string[] {
48
+ const connections = findConnectionsUsingCredential(credentialAccount);
49
+ if (connections.length === 0) {
50
+ return connections;
51
+ }
52
+ if (!force) {
53
+ const list = connections.map((name) => `"${name}"`).join(", ");
54
+ throw new CredentialInUseError(
55
+ connections,
56
+ `Credential ${credentialAccount} is in use by ${
57
+ connections.length === 1 ? "connection" : "connections"
58
+ } ${list}. ${
59
+ connections.length === 1 ? "It" : "They"
60
+ } will stop working without it. Delete it anyway to continue.`,
61
+ );
62
+ }
63
+ log.warn(
64
+ { credential: credentialAccount, connections },
65
+ "Force-deleting a credential that provider connections depend on. Those connections can no longer dispatch until a new credential is added",
66
+ );
67
+ return connections;
68
+ }
69
+
70
+ /**
71
+ * Drop cached adapters built around a deleted credential so the next dispatch
72
+ * re-resolves auth and reports the missing credential instead of replaying a
73
+ * cached adapter holding the removed key.
74
+ */
75
+ export function invalidateConnectionsAfterCredentialDelete(
76
+ affectedConnections: string[],
77
+ ): void {
78
+ if (affectedConnections.length === 0) {
79
+ return;
80
+ }
81
+ // The cache is keyed by connection + model + provider, so entries for one
82
+ // connection are not addressable individually; clearing it costs the next
83
+ // dispatch per connection one credential re-read.
84
+ clearConnectionProviderCache();
85
+ }
@@ -16,22 +16,13 @@
16
16
 
17
17
  import { z } from "zod";
18
18
 
19
- import {
20
- ACP_SERVICE,
21
- AcpCredentialFormatError,
22
- assertAcpCredentialFormat,
23
- } from "../../acp/acp-credentials.js";
19
+ import { AcpCredentialFormatError } from "../../acp/acp-credentials.js";
24
20
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
25
21
  import {
26
22
  fetchManagedCatalog,
27
23
  type ManagedCredentialDescriptor,
28
24
  } from "../../credential-execution/managed-catalog.js";
29
25
  import { buildForChatSentinel } from "../../daemon/chat-credential-redaction.js";
30
- import {
31
- isNonSecretPlatformField,
32
- scrubStoredCredentialFromTranscripts,
33
- } from "../../daemon/credential-transcript-scrub.js";
34
- import { syncManualTokenConnection } from "../../oauth/manual-token-connection.js";
35
26
  import {
36
27
  disconnectOAuthProvider,
37
28
  getConnectionByProvider,
@@ -39,14 +30,11 @@ import {
39
30
  type OAuthConnectionRow,
40
31
  } from "../../oauth/oauth-store.js";
41
32
  import { credentialKey } from "../../security/credential-key.js";
42
- import { normalizeSecretValue } from "../../security/secret-normalize.js";
43
33
  import {
44
34
  deleteSecureKeyAsync,
45
35
  getActiveBackendInfoAsync,
46
- getActiveBackendName,
47
36
  getSecureKeyAsync,
48
37
  getSecureKeyResultAsync,
49
- setSecureKeyAsync,
50
38
  } from "../../security/secure-keys.js";
51
39
  import {
52
40
  assertMetadataWritable,
@@ -55,19 +43,24 @@ import {
55
43
  getCredentialMetadata,
56
44
  getCredentialMetadataById,
57
45
  listCredentialMetadata,
58
- upsertCredentialMetadata,
59
46
  } from "../../tools/credentials/metadata-store.js";
60
47
  import type { CredentialInjectionTemplate } from "../../tools/credentials/policy-types.js";
61
- import { getLogger } from "../../util/logger.js";
48
+ import {
49
+ CredentialStorageError,
50
+ InvalidCredentialInputError,
51
+ storeCredentialValue,
52
+ } from "../../tools/credentials/store.js";
62
53
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
63
54
  import { recordForChatMint } from "../for-chat-mint-registry.js";
64
55
  import { recordRevealSuccess } from "../reveal-success-registry.js";
56
+ import {
57
+ assertCredentialNotInUse,
58
+ invalidateConnectionsAfterCredentialDelete,
59
+ } from "./credential-in-use.js";
65
60
  import { InjectionTemplateSchema } from "./credential-prompt-routes.js";
66
61
  import { BadRequestError, InternalError } from "./errors.js";
67
62
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
68
63
 
69
- const log = getLogger("credential-routes");
70
-
71
64
  // ---------------------------------------------------------------------------
72
65
  // Shared helpers
73
66
  // ---------------------------------------------------------------------------
@@ -458,74 +451,29 @@ async function handleCredentialsSet({ body }: RouteHandlerArgs) {
458
451
  throw new BadRequestError("value is required");
459
452
  }
460
453
 
461
- const normalizedValue = normalizeSecretValue(value);
462
- if (normalizedValue.length === 0) {
463
- throw new BadRequestError("value is required");
464
- }
465
-
466
- // Reject an Anthropic API key pasted into the ACP OAuth-token field (a 401
467
- // footgun) as a 400 rather than letting the guard surface as a 500.
468
- if (service === ACP_SERVICE) {
469
- try {
470
- assertAcpCredentialFormat(field, normalizedValue);
471
- } catch (err) {
472
- if (err instanceof AcpCredentialFormatError) {
473
- throw new BadRequestError(err.message);
474
- }
475
- throw err;
454
+ try {
455
+ return await storeCredentialValue({
456
+ service,
457
+ field,
458
+ value,
459
+ alias: label,
460
+ usageDescription: description,
461
+ allowedTools,
462
+ allowedDomains,
463
+ injectionTemplates,
464
+ });
465
+ } catch (err) {
466
+ if (
467
+ err instanceof InvalidCredentialInputError ||
468
+ err instanceof AcpCredentialFormatError
469
+ ) {
470
+ throw new BadRequestError(err.message);
476
471
  }
477
- }
478
-
479
- assertMetadataWritable();
480
-
481
- const key = credentialKey(service, field);
482
- const stored = await setSecureKeyAsync(key, normalizedValue);
483
- if (!stored) {
484
- throw new InternalError(
485
- `Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
486
- );
487
- }
488
-
489
- // The stored plaintext may already sit in recent transcripts: the user
490
- // message that pasted it, the persisted tool_use input, the tool result
491
- // echoing the command. This route is the scrub seam — not
492
- // setSecureKeyAsync, which also fires on OAuth refresh rotations and MCP
493
- // header writes whose values never transited a transcript. The scrub runs
494
- // immediately after the secure-store write, BEFORE the metadata upsert and
495
- // connection sync: those side effects can throw (oauth-store work), and a
496
- // stored-but-unscrubbed secret must not depend on them succeeding. The
497
- // credential IS stored at this point; the scrub is best-effort hygiene and
498
- // must stay invisible to the caller.
499
- if (!isNonSecretPlatformField(service, field)) {
500
- try {
501
- const scrubbed =
502
- await scrubStoredCredentialFromTranscripts(normalizedValue);
503
- log.info(
504
- { service, field, ...scrubbed },
505
- "Credential stored; scrubbed value from recent transcripts",
506
- );
507
- } catch (err) {
508
- log.warn(
509
- { err, service, field },
510
- "Credential stored, but transcript scrub failed",
511
- );
472
+ if (err instanceof CredentialStorageError) {
473
+ throw new InternalError(err.message);
512
474
  }
475
+ throw err;
513
476
  }
514
-
515
- const metadata = upsertCredentialMetadata(service, field, {
516
- alias: label,
517
- usageDescription: description,
518
- allowedTools,
519
- allowedDomains,
520
- injectionTemplates,
521
- });
522
- await syncManualTokenConnection(service);
523
-
524
- return {
525
- credentialId: metadata.credentialId,
526
- service,
527
- field,
528
- };
529
477
  }
530
478
 
531
479
  async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
@@ -533,9 +481,10 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
533
481
  throw new BadRequestError("Request body is required");
534
482
  }
535
483
 
536
- const { service, field } = body as {
484
+ const { service, field, force } = body as {
537
485
  service?: string;
538
486
  field?: string;
487
+ force?: boolean;
539
488
  };
540
489
 
541
490
  if (!service || typeof service !== "string") {
@@ -557,6 +506,7 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
557
506
  service === "slack_channel" && field === "user_token";
558
507
 
559
508
  const key = credentialKey(service, field);
509
+ const affectedConnections = assertCredentialNotInUse(key, force === true);
560
510
  const existing = await getSecureKeyAsync(key);
561
511
  const deleteResult =
562
512
  existing != null ? await deleteSecureKeyAsync(key) : "not-found";
@@ -567,6 +517,11 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
567
517
  );
568
518
  }
569
519
 
520
+ // Cached provider adapters hold the deleted credential, so they are dropped
521
+ // as soon as the secure vault no longer has it: a later failure in this
522
+ // handler must not leave them dispatching with a credential that is gone.
523
+ invalidateConnectionsAfterCredentialDelete(affectedConnections);
524
+
570
525
  const metadataDeleted = deleteCredentialMetadata(service, field);
571
526
 
572
527
  // Clean up OAuth connection (best-effort).
@@ -593,7 +548,9 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
593
548
  throw new BadRequestError("Credential not found");
594
549
  }
595
550
 
596
- return { service, field };
551
+ invalidateConnectionsAfterCredentialDelete(affectedConnections);
552
+
553
+ return { service, field, affectedConnections };
597
554
  }
598
555
 
599
556
  async function handleCredentialsStatus() {
@@ -746,15 +703,28 @@ export const ROUTES: RouteDefinition[] = [
746
703
  },
747
704
  summary: "Delete a credential",
748
705
  description:
749
- "Remove a secret, its metadata, and any associated OAuth connection from the vault.",
706
+ "Remove a secret, its metadata, and any associated OAuth connection from the vault. " +
707
+ "Refused with CREDENTIAL_IN_USE while an LLM provider connection resolves its auth " +
708
+ "through the credential, unless `force` is set.",
750
709
  tags: ["credentials"],
751
710
  requestBody: z.object({
752
711
  service: z.string().describe("Service namespace"),
753
712
  field: z.string().describe("Field name"),
713
+ force: z
714
+ .boolean()
715
+ .optional()
716
+ .describe(
717
+ "Delete even when provider connections depend on the credential",
718
+ ),
754
719
  }),
755
720
  responseBody: z.object({
756
721
  service: z.string(),
757
722
  field: z.string(),
723
+ affectedConnections: z
724
+ .array(z.string())
725
+ .describe(
726
+ "Provider connections that depended on the deleted credential",
727
+ ),
758
728
  }),
759
729
  handler: handleCredentialsDelete,
760
730
  },
@@ -7,8 +7,10 @@
7
7
  */
8
8
  import type { KnownBlock } from "@slack/types";
9
9
 
10
+ import { audienceForReader } from "../../channels/message-audience.js";
10
11
  import type { ChannelId } from "../../channels/types.js";
11
12
  import type { TrustContext } from "../../daemon/trust-context-types.js";
13
+ import { editChannelMessage } from "../../messaging/providers/index.js";
12
14
  import { getLogger } from "../../util/logger.js";
13
15
  import { resolveCapabilities } from "../capabilities.js";
14
16
  import type { ApprovalDecisionResult } from "../channel-approval-types.js";
@@ -33,18 +35,6 @@ import { deliverStaleApprovalReply } from "./guardian-approval-reply-helpers.js"
33
35
 
34
36
  const log = getLogger("runtime-http");
35
37
 
36
- /**
37
- * Resolve the Slack ephemeral user ID when the source channel is Slack.
38
- * Returns `undefined` for non-Slack channels so callers can pass the
39
- * result directly to `ephemeralUserId` without branching.
40
- */
41
- function slackEphemeralUserId(
42
- sourceChannel: ChannelId,
43
- userId: string | undefined,
44
- ): string | undefined {
45
- return sourceChannel === "slack" && userId ? userId : undefined;
46
- }
47
-
48
38
  export interface ApprovalInterceptionParams {
49
39
  conversationId: string;
50
40
  callbackData?: string;
@@ -170,7 +160,11 @@ export async function handleApprovalInterception(
170
160
  errorLogMessage:
171
161
  "Failed to deliver guardian-pending notice to non-guardian actor (pre-row guard)",
172
162
  errorLogContext: { conversationId },
173
- ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
163
+ audience: audienceForReader(
164
+ sourceChannel,
165
+ conversationExternalId,
166
+ actorExternalId,
167
+ ),
174
168
  });
175
169
  return { handled: true, type: "assistant_turn" };
176
170
  }
@@ -205,8 +199,9 @@ export async function handleApprovalInterception(
205
199
  "Blocking guardian-class approval decision: acting principal missing or does not match the bound guardian principal",
206
200
  );
207
201
  if (replyCallbackUrl) {
208
- const ephemeralUser = slackEphemeralUserId(
202
+ const rejectionAudience = audienceForReader(
209
203
  sourceChannel,
204
+ conversationExternalId,
210
205
  actorExternalId,
211
206
  );
212
207
  try {
@@ -214,7 +209,10 @@ export async function handleApprovalInterception(
214
209
  chatId: conversationExternalId,
215
210
  text: "Sorry, I couldn't process that. Please try again.",
216
211
  assistantId,
217
- ...(ephemeralUser ? { ephemeral: true, user: ephemeralUser } : {}),
212
+ // Stated rather than spread. A rejection that loses its
213
+ // audience becomes a public one, so the key is always present
214
+ // and the compiler checks it.
215
+ audience: rejectionAudience,
218
216
  });
219
217
  } catch (err) {
220
218
  log.error(
@@ -273,11 +271,10 @@ export async function handleApprovalInterception(
273
271
  decisionOutcome === "approved" ? "\u2713" : "\u2717";
274
272
  const statusLabel =
275
273
  decisionOutcome === "approved" ? "Approved" : "Denied";
276
- deliverChannelReply(replyCallbackUrl, {
274
+ editChannelMessage(replyCallbackUrl, {
277
275
  chatId: conversationExternalId,
276
+ messageId: approvalMessageTs,
278
277
  text: `${statusEmoji} ${statusLabel}`,
279
- messageTs: approvalMessageTs,
280
- assistantId,
281
278
  }).catch((err) => {
282
279
  log.error(
283
280
  { err, conversationId, messageTs: approvalMessageTs },
@@ -362,7 +359,11 @@ export async function handleApprovalInterception(
362
359
  toolName: pending.length > 0 ? pending[0].toolName : undefined,
363
360
  },
364
361
  errorLogContext: { conversationId },
365
- ephemeralUserId: slackEphemeralUserId(sourceChannel, actorExternalId),
362
+ audience: audienceForReader(
363
+ sourceChannel,
364
+ conversationExternalId,
365
+ actorExternalId,
366
+ ),
366
367
  });
367
368
 
368
369
  return { handled: true, type: "assistant_turn" };
@@ -395,12 +396,11 @@ function editStaleSlackApprovalMessage(params: {
395
396
  elements: [{ type: "mrkdwn", text: statusText }],
396
397
  },
397
398
  ];
398
- deliverChannelReply(params.replyCallbackUrl, {
399
+ editChannelMessage(params.replyCallbackUrl, {
399
400
  chatId: params.chatId,
401
+ messageId: params.messageTs,
400
402
  text: statusText,
401
403
  blocks,
402
- messageTs: params.messageTs,
403
- assistantId: params.assistantId,
404
404
  }).catch((err) => {
405
405
  log.error(
406
406
  {
@@ -3,6 +3,7 @@
3
3
  * identity mismatch notices, reminders, etc.) that were duplicated across
4
4
  * guardian-approval-interception.ts.
5
5
  */
6
+ import type { MessageAudience } from "@vellumai/gateway-client";
6
7
  import type pino from "pino";
7
8
 
8
9
  import type { ChannelId } from "../../channels/types.js";
@@ -45,7 +46,8 @@ interface DeliverApprovalReplyParams {
45
46
  * sees the message. Used to keep approval-related noise out of shared
46
47
  * channels.
47
48
  */
48
- ephemeralUserId?: string;
49
+ /** Restricts the reply to one reader. Absent means the room. */
50
+ audience?: MessageAudience;
49
51
  }
50
52
 
51
53
  /**
@@ -61,7 +63,7 @@ async function composeAndDeliver(
61
63
  chatId,
62
64
  assistantId,
63
65
  approvalCopyGenerator,
64
- ephemeralUserId,
66
+ audience,
65
67
  } = params;
66
68
 
67
69
  const text = await composeApprovalMessageGenerative(
@@ -74,9 +76,8 @@ async function composeAndDeliver(
74
76
  text,
75
77
  assistantId,
76
78
  };
77
- if (ephemeralUserId) {
78
- payload.ephemeral = true;
79
- payload.user = ephemeralUserId;
79
+ if (audience) {
80
+ payload.audience = audience;
80
81
  }
81
82
  await deliverChannelReply(replyCallbackUrl, payload);
82
83
  }
@@ -120,7 +121,8 @@ export interface DeliverStaleApprovalReplyParams {
120
121
  * When set, deliver via `chat.postEphemeral` so only this Slack user
121
122
  * sees the message. Keeps approval noise out of shared channels.
122
123
  */
123
- ephemeralUserId?: string;
124
+ /** Restricts the reply to one reader. Absent means the room. */
125
+ audience?: MessageAudience;
124
126
  }
125
127
 
126
128
  /**
@@ -134,12 +136,11 @@ export interface DeliverStaleApprovalReplyParams {
134
136
  export async function deliverStaleApprovalReply(
135
137
  params: DeliverStaleApprovalReplyParams,
136
138
  ): Promise<void> {
137
- const { scenario, sourceChannel, extraContext, ephemeralUserId, ...rest } =
138
- params;
139
+ const { scenario, sourceChannel, extraContext, audience, ...rest } = params;
139
140
 
140
141
  const replyParams: DeliverApprovalReplyParams = {
141
142
  ...rest,
142
- ephemeralUserId,
143
+ audience,
143
144
  context: {
144
145
  scenario,
145
146
  channel: sourceChannel,
@@ -190,7 +191,8 @@ export interface DeliverIdentityMismatchReplyParams {
190
191
  * When set, deliver via `chat.postEphemeral` so only this Slack user
191
192
  * sees the message.
192
193
  */
193
- ephemeralUserId?: string;
194
+ /** Restricts the reply to one reader. Absent means the room. */
195
+ audience?: MessageAudience;
194
196
  }
195
197
 
196
198
  /**
@@ -200,11 +202,11 @@ export interface DeliverIdentityMismatchReplyParams {
200
202
  export async function deliverIdentityMismatchReply(
201
203
  params: DeliverIdentityMismatchReplyParams,
202
204
  ): Promise<void> {
203
- const { sourceChannel, ephemeralUserId, ...rest } = params;
205
+ const { sourceChannel, audience, ...rest } = params;
204
206
 
205
207
  await deliverApprovalReply({
206
208
  ...rest,
207
- ephemeralUserId,
209
+ audience,
208
210
  context: {
209
211
  scenario: "guardian_identity_mismatch",
210
212
  channel: sourceChannel,