@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
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Tests for the confirmation → guardian-request promotion, including the
3
- * in-flight race: a confirmation resolved while the fire-and-forget gateway
4
- * create is still pending must not strand a pending tool_approval row.
2
+ * Tests for the confirmation to guardian-request promotion, including the
3
+ * in-flight race (a confirmation resolved while the fire-and-forget gateway
4
+ * create is still pending must not strand a pending tool_approval row) and
5
+ * the deadline, which is derived from the approval-window resolver rather
6
+ * than restated as a constant.
5
7
  */
6
8
 
7
9
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -52,6 +54,15 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
52
54
  },
53
55
  }));
54
56
 
57
+ // Deliberately not 300_000, the resolver's default. A fixture on the default
58
+ // passes whether the deadline is read from the resolver or restated as a
59
+ // constant beside it; this value only matches if it is read.
60
+ const APPROVAL_WINDOW_MS = 900_000;
61
+
62
+ mock.module("../tools/tool-approval-handler.js", () => ({
63
+ resolveInlineGrantWaitMs: () => APPROVAL_WINDOW_MS,
64
+ }));
65
+
55
66
  import { asConversation } from "../__tests__/helpers/mock-conversation.js";
56
67
  import type { AssistantEvent } from "../api/index.js";
57
68
  import type { TrustContext } from "../daemon/trust-context-types.js";
@@ -86,6 +97,20 @@ describe("createGuardianRequestForConfirmation", () => {
86
97
  expect(bridgeCalls).toHaveLength(1);
87
98
  });
88
99
 
100
+ test("takes the row deadline from the approval-window resolver", async () => {
101
+ const before = Date.now();
102
+ await createGuardianRequestForConfirmation(MSG, "conv-1");
103
+ const after = Date.now();
104
+
105
+ expect(createCalls).toHaveLength(1);
106
+ const expiresAt = createCalls[0].expiresAt as number;
107
+
108
+ // Bracketed against the clock either side of the call rather than compared
109
+ // to a single Date.now(), so the assertion cannot flake on a slow tick.
110
+ expect(expiresAt).toBeGreaterThanOrEqual(before + APPROVAL_WINDOW_MS);
111
+ expect(expiresAt).toBeLessThanOrEqual(after + APPROVAL_WINDOW_MS);
112
+ });
113
+
89
114
  test("expires the row and skips the bridge when the confirmation resolved mid-create", async () => {
90
115
  confirmationPending = false;
91
116
 
@@ -55,6 +55,7 @@ export async function createGuardianRequestForConfirmation(
55
55
  { summarizeToolInput },
56
56
  { DAEMON_INTERNAL_ASSISTANT_ID },
57
57
  { bridgeConfirmationRequestToGuardian },
58
+ { resolveInlineGrantWaitMs },
58
59
  ] = await Promise.all([
59
60
  import("../daemon/conversation-registry.js"),
60
61
  import("../channels/gateway-guardian-requests.js"),
@@ -62,6 +63,7 @@ export async function createGuardianRequestForConfirmation(
62
63
  import("../tools/tool-input-summary.js"),
63
64
  import("../runtime/assistant-scope.js"),
64
65
  import("../runtime/confirmation-request-guardian-bridge.js"),
66
+ import("../tools/tool-approval-handler.js"),
65
67
  ]);
66
68
 
67
69
  const conversation = findConversation(conversationId);
@@ -100,7 +102,9 @@ export async function createGuardianRequestForConfirmation(
100
102
  activityText: activityRaw ? redactSecrets(activityRaw) : undefined,
101
103
  executionTarget: msg.executionTarget,
102
104
  status: "pending",
103
- expiresAt: Date.now() + 5 * 60 * 1000,
105
+ // The row stays decidable exactly as long as the prompt stays parked:
106
+ // one budget, so raising `timeouts.permissionTimeoutSec` moves both.
107
+ expiresAt: Date.now() + resolveInlineGrantWaitMs(),
104
108
  });
105
109
 
106
110
  // The prompt is actionable before this fire-and-forget create lands, so
@@ -87,6 +87,7 @@ import {
87
87
  type ConversationCreateType,
88
88
  type ConversationOrigin,
89
89
  isHiddenMessageMetadata,
90
+ isSystemCardMetadata,
90
91
  PINNED_GROUP_ID,
91
92
  UNGROUPED_GROUP_ID,
92
93
  } from "./conversation-types.js";
@@ -259,6 +260,14 @@ const backgroundToolCompletionMetadataSchema = z.object({
259
260
 
260
261
  export const messageMetadataSchema = z
261
262
  .object({
263
+ /**
264
+ * Epoch ms the content actually happened, when that differs from when
265
+ * the row was written. Set wherever persistence lags the event: a queued
266
+ * turn draining, or channel history imported long after the fact.
267
+ * History serialization prefers it over `createdAt` for the display
268
+ * timestamp.
269
+ */
270
+ sentAt: z.number().optional(),
262
271
  userMessageChannel: channelIdSchema.optional(),
263
272
  assistantMessageChannel: channelIdSchema.optional(),
264
273
  userMessageInterface: interfaceIdSchema.optional(),
@@ -413,13 +422,15 @@ export {
413
422
  } from "./conversation-types.js";
414
423
 
415
424
  /**
416
- * `messageKind` value marking a daemon-authored system card — a pre-composed
417
- * status reply (the /compact, /clean, and summarize-up-to result cards) that
418
- * bypasses the agent loop. Cards render as standalone system notices, never
419
- * as the assistant persona speaking, and never merge into adjacent assistant
420
- * display turns.
425
+ * The system-card `messageKind` marker and its predicate live in the
426
+ * `conversation-types` leaf so a caller that only stamps or classifies the
427
+ * marker does not pull in this module's DB graph, and are re-exported here
428
+ * alongside the schema that carries them.
421
429
  */
422
- export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
430
+ export {
431
+ isSystemCardMetadata,
432
+ SYSTEM_CARD_MESSAGE_KIND,
433
+ } from "./conversation-types.js";
423
434
 
424
435
  /**
425
436
  * `messageKind` value marking the synthetic assistant row the agent loop
@@ -431,18 +442,6 @@ export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
431
442
  */
432
443
  export const PROVIDER_ERROR_MESSAGE_KIND = "provider_error";
433
444
 
434
- /**
435
- * Shared predicate for the system-card marker on assistant-message metadata
436
- * (see the `messageKind` field on {@link messageMetadataSchema}). One
437
- * definition so display merging, transcript rendering, and turn grouping
438
- * cannot drift.
439
- */
440
- export function isSystemCardMetadata(
441
- metadata: Record<string, unknown> | null | undefined,
442
- ): boolean {
443
- return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
444
- }
445
-
446
445
  /**
447
446
  * Shared predicate for the provider-error marker on assistant-message
448
447
  * metadata (see the `messageKind` field on {@link messageMetadataSchema}).
@@ -2467,7 +2466,7 @@ export function getMessages(conversationId: string): MessageRow[] {
2467
2466
  * offsets until the target is reached (capped at a reasonable maximum to bound
2468
2467
  * scan cost).
2469
2468
  */
2470
- export function selectSlackMetaCandidateMetadata(
2469
+ export function selectProviderMetaCandidateMetadata(
2471
2470
  conversationId: string,
2472
2471
  limit: number,
2473
2472
  offset = 0,
@@ -2482,10 +2481,14 @@ export function selectSlackMetaCandidateMetadata(
2482
2481
  lineageFilter(conversationId),
2483
2482
  opts?.includeFlatLegacy
2484
2483
  ? or(
2484
+ like(messages.metadata, '%"providerMeta"%'),
2485
2485
  like(messages.metadata, '%"slackMeta"%'),
2486
2486
  like(messages.metadata, '%"source":"slack"%'),
2487
2487
  )
2488
- : like(messages.metadata, '%"slackMeta"%'),
2488
+ : or(
2489
+ like(messages.metadata, '%"providerMeta"%'),
2490
+ like(messages.metadata, '%"slackMeta"%'),
2491
+ ),
2489
2492
  ),
2490
2493
  )
2491
2494
  .orderBy(asc(messages.createdAt))
@@ -199,7 +199,8 @@ function surfacedVisibilitySql(alias = "conversations"): string {
199
199
  * explicitly surfaced. {@link standardListingVisibilitySql} already admits
200
200
  * such rows through its surfaced and custom-group arms, so a caller that
201
201
  * must distinguish "visible in the listing" from "user-facing foreground"
202
- * (unread counting) applies this on top.
202
+ * (unread counting, the section index, a `foregroundOnly` list read) applies
203
+ * this on top.
203
204
  *
204
205
  * The SQL twin of `isBackgroundConversation` in the web client's
205
206
  * `utils/conversation-predicates.ts`.
@@ -282,6 +283,16 @@ export interface ConversationListFilter {
282
283
  * the rows its attention surfaces need instead of scanning for them.
283
284
  */
284
285
  needsAttention?: true;
286
+ /**
287
+ * Restrict to user-facing foreground conversations: drop the automated
288
+ * background/scheduled rows the standard listing admits through its
289
+ * custom-group arm ({@link notBackgroundVisibilitySql}, the same predicate
290
+ * the unread count and the section index apply on top of visibility).
291
+ * Only `true` narrows; omit to keep every visible row. A client that
292
+ * needs the newest conversation a user can open can ask for it in one
293
+ * row instead of paging past runs it would skip.
294
+ */
295
+ foregroundOnly?: true;
285
296
  }
286
297
 
287
298
  export interface ConversationListQuery extends ConversationListFilter {
@@ -364,6 +375,7 @@ function conversationListWhere(filter: ConversationListFilter) {
364
375
  originChannel,
365
376
  groupId,
366
377
  needsAttention,
378
+ foregroundOnly,
367
379
  } = filter;
368
380
  return and(
369
381
  conversationTypeClause(conversationType),
@@ -371,6 +383,7 @@ function conversationListWhere(filter: ConversationListFilter) {
371
383
  originChannel ? originChannelClause(originChannel) : undefined,
372
384
  groupId ? groupIdClause(groupId) : undefined,
373
385
  ...(needsAttention ? unseenAttentionStateConditions() : []),
386
+ foregroundOnly ? sql.raw(notBackgroundVisibilitySql()) : undefined,
374
387
  );
375
388
  }
376
389
 
@@ -135,6 +135,28 @@ export function isBackgroundEventMetadata(
135
135
  return typeof metadata?.backgroundEventSource === "string";
136
136
  }
137
137
 
138
+ /**
139
+ * `messageKind` value marking a daemon-authored system card: a notice that
140
+ * bypasses the agent loop (the /compact, /clean, and summarize-up-to result
141
+ * cards, and plugin notices about what a turn did to the user's input). Cards
142
+ * render as standalone system notices, never as the assistant persona
143
+ * speaking, and never merge into adjacent assistant display turns.
144
+ *
145
+ * Lives in this leaf so a caller that only stamps or classifies the marker
146
+ * does not pull in `conversation-crud`'s DB graph.
147
+ */
148
+ export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
149
+
150
+ /**
151
+ * Shared predicate for the system-card marker on assistant-message metadata,
152
+ * so display merging, transcript rendering, and turn grouping cannot drift.
153
+ */
154
+ export function isSystemCardMetadata(
155
+ metadata: Record<string, unknown> | null | undefined,
156
+ ): boolean {
157
+ return metadata?.messageKind === SYSTEM_CARD_MESSAGE_KIND;
158
+ }
159
+
138
160
  /**
139
161
  * True when a role-`"user"` row is internal scaffolding rather than a person's
140
162
  * prompt: a daemon-injected run lifecycle notification (subagent
@@ -5,15 +5,15 @@
5
5
  * finding messages by source identifiers, and managing raw payload storage.
6
6
  */
7
7
 
8
- import { and, desc, eq, isNotNull, ne, or, sql } from "drizzle-orm";
8
+ import { and, desc, eq, isNotNull, like, ne, or, sql } from "drizzle-orm";
9
9
  import { v4 as uuid } from "uuid";
10
10
 
11
11
  import type { ChannelId } from "../channels/types.js";
12
- import { readSlackMetadataFromMessageMetadata } from "../messaging/providers/slack/message-metadata.js";
12
+ import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
13
13
  import type { SlackInboundMessageMetadata } from "../runtime/http-types.js";
14
14
  import { parseJsonSafe } from "../util/json.js";
15
15
  import { isPlainObject } from "../util/object.js";
16
- import { selectSlackMetaCandidateMetadata } from "./conversation-crud.js";
16
+ import { selectProviderMetaCandidateMetadata } from "./conversation-crud.js";
17
17
  import {
18
18
  getConversationByKey,
19
19
  getOrCreateConversation,
@@ -21,7 +21,12 @@ import {
21
21
  } from "./conversation-key-store.js";
22
22
  import type { NonScheduledConversationType } from "./conversation-types.js";
23
23
  import { getDb } from "./db-connection.js";
24
- import { channelInboundEvents, conversations } from "./schema.js";
24
+ import {
25
+ channelInboundEvents,
26
+ conversationKeys,
27
+ conversations,
28
+ messages,
29
+ } from "./schema.js";
25
30
 
26
31
  export interface InboundResult {
27
32
  accepted: boolean;
@@ -33,11 +38,26 @@ export interface InboundResult {
33
38
  export interface RecordInboundOptions {
34
39
  sourceMessageId?: string;
35
40
  sourceThreadId?: string;
41
+ /**
42
+ * Record the event against this conversation instead of resolving one from
43
+ * the address. For events that belong to a message rather than to a chat: a
44
+ * reaction lives in the conversation of the message it was attached to, and
45
+ * resolving from its own address would mint a second one.
46
+ */
47
+ conversationId?: string;
36
48
  }
37
49
 
38
50
  const SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE = 50;
39
51
  const SLACK_LEGACY_THREAD_EVIDENCE_MAX_SCAN = 500;
40
52
 
53
+ /**
54
+ * Rows examined when locating a Slack message the assistant itself posted.
55
+ * Bounded on purpose: the scan runs on the inbound path while the gateway
56
+ * waits for its ack, and reactions land on recent messages, so a cap costs
57
+ * almost no recall and keeps the cost flat as the database grows.
58
+ */
59
+ const SLACK_OUTBOUND_TS_MAX_SCAN = 400;
60
+
41
61
  /**
42
62
  * Channels where an inbound thread id scopes the conversation: a Slack thread
43
63
  * or a Telegram private-chat topic each maps to its own conversation. A
@@ -99,7 +119,7 @@ function legacySlackConversationHasThreadEvidence(
99
119
  SLACK_LEGACY_THREAD_EVIDENCE_BATCH_SIZE,
100
120
  remaining,
101
121
  );
102
- const metadataRows = selectSlackMetaCandidateMetadata(
122
+ const metadataRows = selectProviderMetaCandidateMetadata(
103
123
  conversationId,
104
124
  batchLimit,
105
125
  offset,
@@ -110,12 +130,10 @@ function legacySlackConversationHasThreadEvidence(
110
130
  return false;
111
131
  }
112
132
  for (const metadata of metadataRows) {
113
- const slackMeta = readSlackMetadataFromMessageMetadata(metadata, {
114
- allowFlatLegacy: true,
115
- });
133
+ const meta = readProviderMetadata(metadata, { allowFlatLegacy: true });
116
134
  if (
117
- slackMeta?.channelId === externalChatId &&
118
- slackMeta.threadTs === sourceThreadId
135
+ meta?.conversationExternalId === externalChatId &&
136
+ meta.threadId === sourceThreadId
119
137
  ) {
120
138
  return true;
121
139
  }
@@ -248,6 +266,93 @@ export function findInboundConversationId(
248
266
  * the conversation and tracks the reply. Both must key on the same three
249
267
  * fields for either to mean anything.
250
268
  */
269
+ /**
270
+ * The inbound event already recorded for this address, if any. Read-only twin
271
+ * of the dedup check inside {@link recordInbound}, for callers that must know
272
+ * an event is a redelivery before doing work that recording would otherwise
273
+ * gate (routing a guardian decision, for one).
274
+ */
275
+ export function findInboundEvent(
276
+ sourceChannel: string,
277
+ externalChatId: string,
278
+ externalMessageId: string,
279
+ ): { eventId: string; conversationId: string } | null {
280
+ const row = getDb()
281
+ .select({
282
+ id: channelInboundEvents.id,
283
+ conversationId: channelInboundEvents.conversationId,
284
+ })
285
+ .from(channelInboundEvents)
286
+ .where(
287
+ and(
288
+ eq(channelInboundEvents.sourceChannel, sourceChannel),
289
+ eq(channelInboundEvents.externalChatId, externalChatId),
290
+ eq(channelInboundEvents.externalMessageId, externalMessageId),
291
+ ),
292
+ )
293
+ .get();
294
+ return row ? { eventId: row.id, conversationId: row.conversationId } : null;
295
+ }
296
+
297
+ /**
298
+ * The conversation holding the message with this provider id, found by reading
299
+ * the metadata the assistant's own posts carry.
300
+ *
301
+ * Reads through `readProviderMetadata`, so it matches any channel that
302
+ * describes its rows in the neutral shape as well as Slack's own envelope.
303
+ *
304
+ * `findMessageBySourceId` covers every message that arrived as an inbound
305
+ * event. It cannot see what the assistant posted, because an outbound reply
306
+ * opens no inbound event, so a reaction on the assistant's own message needs
307
+ * this. The search is confined to conversations already bound to the same
308
+ * Slack channel and to the most recent {@link SLACK_OUTBOUND_TS_MAX_SCAN}
309
+ * rows among them; beyond that it gives up and the caller drops the
310
+ * annotation, which is the same outcome as never finding it at all.
311
+ */
312
+ export function findSlackConversationByMessageTs(
313
+ externalChatId: string,
314
+ channelTs: string,
315
+ ): string | null {
316
+ const db = getDb();
317
+ const keyPrefix = `${CONVERSATION_KEY_SCOPE}:slack:${externalChatId}`;
318
+ const rows = db
319
+ .select({
320
+ conversationId: messages.conversationId,
321
+ metadata: messages.metadata,
322
+ })
323
+ .from(messages)
324
+ .innerJoin(
325
+ conversationKeys,
326
+ eq(conversationKeys.conversationId, messages.conversationId),
327
+ )
328
+ .where(
329
+ and(
330
+ or(
331
+ eq(conversationKeys.conversationKey, keyPrefix),
332
+ like(conversationKeys.conversationKey, `${keyPrefix}:thread:%`),
333
+ ),
334
+ or(
335
+ like(messages.metadata, '%"providerMeta"%'),
336
+ like(messages.metadata, '%"slackMeta"%'),
337
+ ),
338
+ ),
339
+ )
340
+ .orderBy(desc(messages.createdAt))
341
+ .limit(SLACK_OUTBOUND_TS_MAX_SCAN)
342
+ .all();
343
+
344
+ for (const row of rows) {
345
+ const meta = readProviderMetadata(row.metadata, { allowFlatLegacy: true });
346
+ if (
347
+ meta?.conversationExternalId === externalChatId &&
348
+ meta.messageId === channelTs
349
+ ) {
350
+ return row.conversationId;
351
+ }
352
+ }
353
+ return null;
354
+ }
355
+
251
356
  export function recordInbound(
252
357
  sourceChannel: string,
253
358
  externalChatId: string,
@@ -280,11 +385,13 @@ export function recordInbound(
280
385
  };
281
386
  }
282
387
 
283
- const mapping = resolveInboundConversation(
284
- sourceChannel,
285
- externalChatId,
286
- options?.sourceThreadId,
287
- );
388
+ const mapping = options?.conversationId
389
+ ? { conversationId: options.conversationId }
390
+ : resolveInboundConversation(
391
+ sourceChannel,
392
+ externalChatId,
393
+ options?.sourceThreadId,
394
+ );
288
395
  const now = Date.now();
289
396
  const eventId = uuid();
290
397
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Guards that importing `@vellumai/plugin-api` does not force a DB-graph
3
+ * module's named exports to resolve.
4
+ *
5
+ * Plugin suites routinely partial-mock modules in the persistence/embeddings
6
+ * graph (a handful of functions, not the whole surface). `mock.module` replaces
7
+ * the module for every importer, so a static edge from plugin-api into that
8
+ * graph makes some unrelated host module import a name the partial mock does not
9
+ * define, and the entire suite fails to instantiate with
10
+ * `SyntaxError: Export named '...' not found in module`. Facades over DB-graph
11
+ * writers therefore load them through a dynamic `import()` inside the wrapper.
12
+ *
13
+ * The probe runs in a subprocess: `mock.module` is process-wide and permanent,
14
+ * so mocking the embedding backend in this process would leak into every other
15
+ * suite sharing the test process.
16
+ */
17
+
18
+ import { spawnSync } from "node:child_process";
19
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { afterEach, describe, expect, test } from "bun:test";
23
+
24
+ const PLUGIN_API_DIR = join(import.meta.dir, "..");
25
+
26
+ let probeDir: string | null = null;
27
+
28
+ afterEach(() => {
29
+ if (probeDir !== null) {
30
+ rmSync(probeDir, { recursive: true, force: true });
31
+ probeDir = null;
32
+ }
33
+ });
34
+
35
+ describe("plugin-api import graph", () => {
36
+ test("instantiates against a partial-mocked DB-graph module", () => {
37
+ // GIVEN a probe suite that partial-mocks a DB-graph module the way plugin
38
+ // suites do (one function, not the module's full export surface) and then
39
+ // imports the public plugin API surface.
40
+ probeDir = mkdtempSync(join(tmpdir(), "plugin-api-import-graph-"));
41
+ const probe = join(probeDir, "probe.test.ts");
42
+ writeFileSync(
43
+ probe,
44
+ `import { expect, mock, test } from "bun:test";\n` +
45
+ `mock.module(${JSON.stringify(join(PLUGIN_API_DIR, "..", "persistence", "embeddings", "embedding-backend.ts"))}, () => ({\n` +
46
+ ` embedWithBackend: async () => [],\n` +
47
+ `}));\n` +
48
+ `test("plugin-api instantiates", async () => {\n` +
49
+ ` const api = await import(${JSON.stringify(join(PLUGIN_API_DIR, "index.ts"))});\n` +
50
+ ` expect(typeof api.persistSystemCard).toEqual("function");\n` +
51
+ ` expect(typeof api.addMessage).toEqual("function");\n` +
52
+ `});\n`,
53
+ );
54
+
55
+ // WHEN the probe suite runs in its own process.
56
+ const result = spawnSync(process.execPath, ["test", probe], {
57
+ cwd: join(PLUGIN_API_DIR, "..", ".."),
58
+ encoding: "utf8",
59
+ timeout: 60_000,
60
+ });
61
+
62
+ // THEN plugin-api instantiated, so nothing in its static import graph
63
+ // reached a name the partial mock left undefined.
64
+ expect(`${result.stdout}${result.stderr}`).not.toContain("SyntaxError");
65
+ expect(result.status).toBe(0);
66
+ });
67
+ });
@@ -280,6 +280,9 @@ export async function runConversationTurn(
280
280
  await import("../persistence/conversation-crud.js");
281
281
  const { publishConversationListAndMetadataChanged } =
282
282
  await import("../runtime/sync/resource-sync-events.js");
283
+ const { parseInterfaceId } = await import("../channels/types.js");
284
+ const { resolveChannelCapabilities } =
285
+ await import("../daemon/conversation-runtime-assembly.js");
283
286
 
284
287
  // Plugin-driven turns run as the guardian: plugins are installed by the
285
288
  // guardian, so their conversations inherit guardian trust. This lets the
@@ -351,6 +354,28 @@ export async function runConversationTurn(
351
354
  assistantMessageChannel: options.channel.sourceChannel,
352
355
  }
353
356
  : null;
357
+ // The interface the message arrived on. `plugin` is in both the channel
358
+ // and interface unions, matching gateway inbound. Tool gating reads this
359
+ // as `transportInterface`; unset, the loop falls back to `web`.
360
+ const turnInterfaceContext = options.channel
361
+ ? (() => {
362
+ const iface = parseInterfaceId(options.channel.sourceChannel);
363
+ return iface
364
+ ? {
365
+ userMessageInterface: iface,
366
+ assistantMessageInterface: iface,
367
+ }
368
+ : null;
369
+ })()
370
+ : null;
371
+ if (options.channel) {
372
+ conversation.setChannelCapabilities(
373
+ resolveChannelCapabilities(
374
+ options.channel.sourceChannel,
375
+ turnInterfaceContext?.userMessageInterface,
376
+ ),
377
+ );
378
+ }
354
379
  // Carried on the metadata as well, because a queued turn is drained after
355
380
  // this call returns and reads its channel from there. Without it the drain
356
381
  // inherits whichever turn was in flight, which on a shared conversation is
@@ -358,6 +383,7 @@ export async function runConversationTurn(
358
383
  const metadata = {
359
384
  ...PLUGIN_TURN_MESSAGE_METADATA,
360
385
  ...(turnChannelContext ?? {}),
386
+ ...(turnInterfaceContext ?? {}),
361
387
  };
362
388
 
363
389
  // Build the event emitter: fan out to SSE/event-hub subscribers via
@@ -398,6 +424,7 @@ export async function runConversationTurn(
398
424
  }
399
425
 
400
426
  conversation.setTurnChannelContext(turnChannelContext);
427
+ conversation.setTurnInterfaceContext(turnInterfaceContext);
401
428
 
402
429
  const userMessageId = await conversation.processMessage({
403
430
  content: text,
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { credentialInPluginScope } from "./credential-scope.js";
4
+
5
+ describe("credentialInPluginScope", () => {
6
+ test("allows every field under the plugin's own service", () => {
7
+ expect(credentialInPluginScope("imessage", "imessage")).toBe(true);
8
+ expect(credentialInPluginScope("acme", "acme")).toBe(true);
9
+ });
10
+
11
+ test("blocks another service, even when the field matches the plugin name", () => {
12
+ expect(credentialInPluginScope("acme", "openai")).toBe(false);
13
+ expect(credentialInPluginScope("imessage", "openai")).toBe(false);
14
+ });
15
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Whether a plugin may read or write a credential named `service/field`.
3
+ *
4
+ * The plugin's manifest name is the service it owns. Plugin `imessage` may
5
+ * resolve `imessage/api_key` and `imessage/photon_project_id`, and nothing
6
+ * under another service (`openai/api_key`, `openai/imessage`). The check is
7
+ * name equality only; it does not look the credential up.
8
+ */
9
+ export function credentialInPluginScope(
10
+ pluginName: string,
11
+ service: string,
12
+ ): boolean {
13
+ return service === pluginName;
14
+ }
@@ -170,13 +170,25 @@ export { doesSupportVision } from "./vision-support.js";
170
170
  // Resolve a stored credential to its plaintext value — the same value
171
171
  // `assistant credentials reveal` prints — from a UUID or a "service/field"
172
172
  // reference. When a plugin is in context, resolution is scoped to credentials
173
- // whose `field` matches the plugin's manifest name; outside any plugin it is
173
+ // whose service matches the plugin's manifest name; outside any plugin it is
174
174
  // unscoped. Throws CredentialResolutionError when the ref does not resolve, the
175
175
  // store is unreachable, or the credential is out of the plugin's scope.
176
176
  export {
177
177
  CredentialResolutionError,
178
178
  resolveCredential,
179
179
  } from "./resolve-credential.js";
180
+ // Store a credential's plaintext value (the same write `assistant credentials
181
+ // set` performs), creating it or replacing an existing one, named by UUID or a
182
+ // "service/field" reference. A plugin may only write credentials whose service
183
+ // matches its manifest name, and the write fails closed with no plugin in
184
+ // context. Throws CredentialStoreError when there is no calling plugin, the ref
185
+ // is malformed, the value is invalid, the store rejects the write, or the
186
+ // credential is out of the plugin's scope.
187
+ export type {
188
+ StoreCredentialOptions,
189
+ StoredCredentialRef,
190
+ } from "./store-credential.js";
191
+ export { CredentialStoreError, storeCredential } from "./store-credential.js";
180
192
  // Resolve the public URL a third party should deliver to for one of the
181
193
  // plugin's own ingress routes. Which URL is correct depends on how the
182
194
  // assistant is reachable (a managed platform callback route, or a configured
@@ -348,6 +360,12 @@ export {
348
360
  syncMessageToDisk,
349
361
  updateMessageMetadata,
350
362
  } from "../persistence/conversation-plugin-facade.js";
363
+ // System cards: a transcript notice authored by the daemon rather than the
364
+ // assistant persona, for telling the user something a turn did to their input
365
+ // that the model's reply cannot explain (e.g. an attachment that could not be
366
+ // sent). Persisted and pushed to clients; not seated in the turn's working
367
+ // history.
368
+ export { persistSystemCard } from "./system-card.js";
351
369
  // Synthesize text to speech through the assistant's globally configured TTS
352
370
  // provider (ElevenLabs, Fish Audio, etc.). Plugins that need voice output —
353
371
  // e.g. a meeting bot speaking into a live call — use this instead of managing