@vellumai/assistant 0.11.4 → 0.11.5-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/conversation-attachments.test.ts +0 -1
  38. package/src/__tests__/conversation-error.test.ts +17 -0
  39. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  40. package/src/__tests__/conversation-pairing.test.ts +186 -0
  41. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  42. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  43. package/src/__tests__/credential-routes.test.ts +22 -3
  44. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  45. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  46. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  47. package/src/__tests__/dm-backfill.test.ts +63 -0
  48. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  49. package/src/__tests__/edit-propagation.test.ts +107 -4
  50. package/src/__tests__/events-client-registration.test.ts +28 -0
  51. package/src/__tests__/file-write-tool.test.ts +4 -2
  52. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  53. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  54. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  55. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  56. package/src/__tests__/host-shell-tool.test.ts +23 -4
  57. package/src/__tests__/image-conversion.test.ts +143 -1
  58. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  59. package/src/__tests__/input-repairs.test.ts +207 -0
  60. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  61. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  62. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  63. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  64. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  65. package/src/__tests__/mtime-cache.test.ts +2 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  67. package/src/__tests__/openai-provider.test.ts +22 -0
  68. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  69. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  70. package/src/__tests__/platform.test.ts +16 -1
  71. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  72. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  73. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  74. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  75. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  76. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  77. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  78. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  79. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  80. package/src/__tests__/reaction-persistence.test.ts +200 -7
  81. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  82. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  83. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  84. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  85. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  86. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  87. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  88. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  89. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  90. package/src/__tests__/tool-executor.test.ts +63 -38
  91. package/src/__tests__/tool-policy.test.ts +97 -1
  92. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  93. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  94. package/src/__tests__/validate-input.test.ts +243 -7
  95. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  97. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  98. package/src/acp/acp-claude-oauth.ts +38 -21
  99. package/src/acp/prepare-agent-env.ts +146 -55
  100. package/src/agent/loop-tool-dedup.test.ts +161 -0
  101. package/src/agent/loop.ts +68 -22
  102. package/src/api/constants/app-tools.ts +34 -0
  103. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  104. package/src/api/index.ts +19 -0
  105. package/src/api/responses/resource-pressure-status.ts +25 -0
  106. package/src/approvals/guardian-channel-delivery.ts +70 -6
  107. package/src/approvals/guardian-request-resolvers.ts +32 -66
  108. package/src/channels/__tests__/message-audience.test.ts +49 -0
  109. package/src/channels/__tests__/types.test.ts +17 -3
  110. package/src/channels/message-audience.ts +40 -0
  111. package/src/channels/types.ts +16 -12
  112. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  113. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  114. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  115. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  116. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  117. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  118. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  119. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  120. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  121. package/src/cli/commands/conversations.help.ts +34 -0
  122. package/src/cli/commands/conversations.ts +125 -0
  123. package/src/cli/commands/inference-providers.ts +9 -4
  124. package/src/cli/commands/inference.help.ts +9 -4
  125. package/src/cli/commands/keys.help.ts +13 -1
  126. package/src/cli/commands/keys.ts +26 -15
  127. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  128. package/src/cli/commands/memory/index.help.ts +41 -27
  129. package/src/cli/commands/memory/index.ts +2 -0
  130. package/src/cli/commands/memory/memory-v2.ts +58 -54
  131. package/src/cli/commands/memory/memory-validate.ts +18 -0
  132. package/src/cli/commands/monitoring.ts +1 -0
  133. package/src/cli/commands/plugins.help.ts +28 -31
  134. package/src/cli/commands/plugins.ts +10 -0
  135. package/src/cli/commands/skills.help.ts +13 -16
  136. package/src/cli/commands/trust.ts +3 -14
  137. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  138. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  139. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  140. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  141. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  142. package/src/cli/lib/bundled-marketplace.json +13 -0
  143. package/src/cli/lib/daemon-credential-client.ts +25 -3
  144. package/src/cli/lib/install-from-github.ts +35 -24
  145. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  146. package/src/cli/lib/plugin-surfaces.ts +27 -0
  147. package/src/config/feature-flag-registry.json +35 -19
  148. package/src/config/webhook-routing.ts +8 -0
  149. package/src/context/compactor.ts +31 -2
  150. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  151. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  152. package/src/daemon/conversation-error.ts +13 -0
  153. package/src/daemon/conversation-messaging.ts +22 -2
  154. package/src/daemon/conversation-process.ts +11 -0
  155. package/src/daemon/conversation-store.ts +10 -0
  156. package/src/daemon/conversation-surfaces.ts +21 -8
  157. package/src/daemon/conversation.ts +12 -10
  158. package/src/daemon/lifecycle.ts +6 -2
  159. package/src/daemon/message-types/conversations.ts +5 -7
  160. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  161. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  162. package/src/daemon/resource-pressure-guard.ts +387 -0
  163. package/src/daemon/shutdown-handlers.ts +2 -0
  164. package/src/daemon/startup-error.ts +16 -0
  165. package/src/daemon/trust-context.ts +14 -10
  166. package/src/daemon/unsendable-image-notice.ts +76 -0
  167. package/src/hooks/registry.ts +65 -51
  168. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  169. package/src/ipc/gateway-client.test.ts +1 -1
  170. package/src/ipc/gateway-client.ts +14 -21
  171. package/src/ipc/socket-cleanup.ts +2 -11
  172. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  173. package/src/messaging/provider-message-metadata.ts +128 -0
  174. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  175. package/src/messaging/providers/channel-transport.ts +90 -13
  176. package/src/messaging/providers/discord/send.ts +16 -0
  177. package/src/messaging/providers/discord/transport.ts +11 -1
  178. package/src/messaging/providers/index.ts +109 -16
  179. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  180. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  181. package/src/messaging/providers/slack/send.test.ts +6 -9
  182. package/src/messaging/providers/slack/send.ts +40 -28
  183. package/src/messaging/providers/slack/transport.ts +22 -26
  184. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  185. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  186. package/src/messaging/read-provider-metadata.test.ts +157 -0
  187. package/src/messaging/read-provider-metadata.ts +51 -0
  188. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  189. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  190. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  191. package/src/notifications/adapters/macos.ts +55 -0
  192. package/src/notifications/adapters/slack.ts +10 -5
  193. package/src/notifications/assistant-reply-producer.ts +37 -4
  194. package/src/notifications/conversation-pairing.ts +111 -21
  195. package/src/notifications/edit-notification.ts +38 -8
  196. package/src/notifications/emit-signal.ts +12 -14
  197. package/src/notifications/home-feed-side-effect.ts +218 -15
  198. package/src/notifications/types.ts +5 -0
  199. package/src/permissions/AGENTS.md +16 -0
  200. package/src/permissions/checker.test.ts +75 -122
  201. package/src/permissions/checker.ts +44 -560
  202. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  203. package/src/permissions/confirmation-guardian-request.ts +5 -1
  204. package/src/persistence/conversation-crud.ts +23 -20
  205. package/src/persistence/conversation-queries.ts +14 -1
  206. package/src/persistence/conversation-types.ts +22 -0
  207. package/src/persistence/delivery-crud.ts +122 -15
  208. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  209. package/src/plugin-api/conversation-turn.ts +27 -0
  210. package/src/plugin-api/credential-scope.test.ts +15 -0
  211. package/src/plugin-api/credential-scope.ts +14 -0
  212. package/src/plugin-api/index.ts +19 -1
  213. package/src/plugin-api/resolve-credential.ts +15 -10
  214. package/src/plugin-api/store-credential.ts +148 -0
  215. package/src/plugin-api/system-card.ts +36 -0
  216. package/src/plugin-api/vision-support.test.ts +82 -0
  217. package/src/plugin-api/vision-support.ts +14 -2
  218. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  219. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  220. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  221. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  222. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  223. package/src/plugins/defaults/main.ts +15 -0
  224. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  225. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  226. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  227. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  228. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  229. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  230. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  231. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  232. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  233. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  234. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  238. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  239. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  240. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  241. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  242. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  243. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  244. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  245. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  246. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  247. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  248. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  249. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  250. package/src/plugins/mtime-cache.ts +1 -1
  251. package/src/plugins/pipeline.ts +14 -7
  252. package/src/plugins/plugin-execution-context.ts +35 -11
  253. package/src/plugins/plugin-tree-walk.ts +63 -4
  254. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  255. package/src/providers/anthropic/client.ts +332 -241
  256. package/src/providers/connection-resolution.ts +2 -1
  257. package/src/providers/content-blocks.ts +22 -0
  258. package/src/providers/gemini/client.ts +5 -2
  259. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  260. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  261. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  262. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  263. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  264. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  265. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  266. package/src/providers/inference/adapter-factory.ts +33 -2
  267. package/src/providers/inference/auth.ts +13 -2
  268. package/src/providers/inference/credential-usage.ts +37 -0
  269. package/src/providers/inference/missing-credential-guard.ts +110 -0
  270. package/src/providers/inference/resolve-auth.ts +7 -7
  271. package/src/providers/media-resolve.ts +176 -15
  272. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  273. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  274. package/src/providers/openai/chat-completions-provider.ts +322 -42
  275. package/src/routes/route-host-protocol.ts +7 -0
  276. package/src/routes/worker.ts +31 -10
  277. package/src/runtime/AGENTS.md +35 -0
  278. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  279. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  280. package/src/runtime/assistant-event-hub.ts +38 -0
  281. package/src/runtime/channel-reply-delivery.ts +54 -31
  282. package/src/runtime/channel-retry-sweep.ts +6 -4
  283. package/src/runtime/effective-capabilities.test.ts +19 -0
  284. package/src/runtime/effective-capabilities.ts +25 -0
  285. package/src/runtime/guardian-reply-router.ts +6 -2
  286. package/src/runtime/http-errors.ts +1 -0
  287. package/src/runtime/http-server.ts +20 -5
  288. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  289. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  290. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  291. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  292. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  293. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  294. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  295. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  296. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  297. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  298. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  299. package/src/runtime/routes/approval-routes.ts +37 -4
  300. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  301. package/src/runtime/routes/canned-message-complete.ts +68 -29
  302. package/src/runtime/routes/client-routes.ts +113 -0
  303. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  304. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  305. package/src/runtime/routes/conversation-routes.ts +36 -22
  306. package/src/runtime/routes/credential-in-use.ts +85 -0
  307. package/src/runtime/routes/credential-routes.ts +55 -85
  308. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  309. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  310. package/src/runtime/routes/identity-routes.ts +5 -121
  311. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  312. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  313. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  314. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  315. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  316. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  317. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  318. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  319. package/src/runtime/routes/index.ts +2 -0
  320. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  321. package/src/runtime/routes/monitoring-routes.ts +9 -1
  322. package/src/runtime/routes/plugins-routes.ts +17 -5
  323. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  324. package/src/runtime/routes/secret-routes.ts +39 -8
  325. package/src/runtime/routes/settings-routes.ts +6 -6
  326. package/src/runtime/routes/surface-action-routes.ts +20 -19
  327. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  328. package/src/runtime/routes/user-route-resolution.ts +18 -1
  329. package/src/runtime/slack-reply-session.test.ts +23 -13
  330. package/src/runtime/slack-reply-session.ts +38 -55
  331. package/src/runtime/web-presence.ts +90 -0
  332. package/src/skills/validate-input.ts +177 -40
  333. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  334. package/src/subagent/consult-context.ts +7 -23
  335. package/src/tools/credentials/broker.ts +24 -79
  336. package/src/tools/credentials/ref-parse.ts +35 -0
  337. package/src/tools/credentials/resolve.ts +4 -10
  338. package/src/tools/credentials/store.ts +168 -0
  339. package/src/tools/credentials/tool-policy.ts +67 -0
  340. package/src/tools/executor.ts +53 -15
  341. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  342. package/src/tools/network/url-safety.ts +9 -16
  343. package/src/tools/network/web-search.ts +21 -3
  344. package/src/tools/permission-checker.ts +43 -52
  345. package/src/tools/schema-transforms.ts +40 -5
  346. package/src/tools/shared/input-repairs.ts +160 -0
  347. package/src/tools/skills/skill-tool-factory.ts +18 -4
  348. package/src/tools/subagent/spawn.ts +0 -1
  349. package/src/tools/tool-approval-handler.ts +16 -10
  350. package/src/tools/tool-types.ts +26 -13
  351. package/src/tools/types.ts +6 -6
  352. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  353. package/src/util/cgroup-memory.ts +3 -0
  354. package/src/util/container-cpu-sampler.ts +250 -0
  355. package/src/util/image-conversion.ts +178 -14
  356. package/src/util/platform.ts +93 -10
  357. package/src/permissions/ipc-risk-types.ts +0 -143
  358. package/src/permissions/risk-types.ts +0 -76
@@ -0,0 +1,310 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ unsendableImageCardText,
5
+ unsendableImageFilenames,
6
+ } from "../daemon/unsendable-image-notice.js";
7
+ import { createInlineAttachment } from "../persistence/attachments-store.js";
8
+ import {
9
+ addMessage,
10
+ createConversation,
11
+ } from "../persistence/conversation-crud.js";
12
+ import { getDb } from "../persistence/db-connection.js";
13
+ import { initializeDb } from "../persistence/db-init.js";
14
+ import {
15
+ isUnsendableImageSource,
16
+ resolveMediaReferences,
17
+ UNSENDABLE_IMAGE_FORMAT_NOTE,
18
+ } from "../providers/media-resolve.js";
19
+ import type { ContentBlock, Message } from "../providers/types.js";
20
+
21
+ await initializeDb();
22
+
23
+ function resetTables() {
24
+ const db = getDb();
25
+ db.run("DELETE FROM message_attachments");
26
+ db.run("DELETE FROM attachments");
27
+ db.run("DELETE FROM messages");
28
+ db.run("DELETE FROM conversations");
29
+ }
30
+
31
+ /** A 1x1 PNG: a real signature plus enough body to be a decodable image. */
32
+ const PNG_B64 =
33
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==";
34
+
35
+ /**
36
+ * A HEIC head (ISO BMFF `ftyp` + the `heic` brand) under a `.png` name, which
37
+ * is what a photo renamed by hand or exported by iOS arrives as.
38
+ */
39
+ const HEIC_B64 = Buffer.from([
40
+ 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, 0x00,
41
+ 0x00, 0x00, 0x00,
42
+ ]).toString("base64");
43
+
44
+ /** A PNG whose signature bytes are damaged, as a partial upload leaves it. */
45
+ const CORRUPT_PNG_B64 = Buffer.from([
46
+ 0x89, 0x50, 0x4e, 0x00, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
47
+ ]).toString("base64");
48
+
49
+ function userMessage(content: ContentBlock[]): Message {
50
+ return { role: "user", content };
51
+ }
52
+
53
+ function inlineImage(mediaType: string, data: string): ContentBlock {
54
+ return {
55
+ type: "image",
56
+ source: { type: "base64", media_type: mediaType, data },
57
+ };
58
+ }
59
+
60
+ async function storedImageRef(
61
+ filename: string,
62
+ mimeType: string,
63
+ dataBase64: string,
64
+ ): Promise<ContentBlock> {
65
+ const conv = createConversation();
66
+ const stored = await createInlineAttachment(
67
+ conv.id,
68
+ conv.createdAt,
69
+ filename,
70
+ mimeType,
71
+ dataBase64,
72
+ );
73
+ return {
74
+ type: "image",
75
+ source: {
76
+ type: "workspace_ref",
77
+ media_type: mimeType,
78
+ attachmentId: stored.id,
79
+ sizeBytes: stored.sizeBytes,
80
+ filename,
81
+ },
82
+ };
83
+ }
84
+
85
+ describe("resolveMediaReferences image validation", () => {
86
+ beforeEach(resetTables);
87
+
88
+ test("sends a valid inline image untouched", async () => {
89
+ // GIVEN a message carrying an inline PNG whose bytes match its declaration
90
+ const message = userMessage([inlineImage("image/png", PNG_B64)]);
91
+
92
+ // WHEN the send boundary resolves it
93
+ const [resolved] = await resolveMediaReferences([message]);
94
+
95
+ // THEN the block is unchanged, and the message is the same object (no
96
+ // allocation or disk read on the clean live-turn fast path)
97
+ expect(resolved).toBe(message);
98
+ });
99
+
100
+ test("drops the unreadable image from a batch and keeps the valid ones", async () => {
101
+ // GIVEN a batch of a valid PNG, a HEIC photo declared as a PNG, and a
102
+ // truncated PNG: the shape of the eight-image upload that lost a whole turn
103
+ const message = userMessage([
104
+ { type: "text", text: "what is in these?" },
105
+ inlineImage("image/png", PNG_B64),
106
+ inlineImage("image/png", HEIC_B64),
107
+ inlineImage("image/png", CORRUPT_PNG_B64),
108
+ ]);
109
+
110
+ // WHEN the send boundary resolves them
111
+ const [resolved] = await resolveMediaReferences([message]);
112
+
113
+ // THEN the valid image still goes to the model
114
+ expect(resolved.content[1]).toEqual(inlineImage("image/png", PNG_B64));
115
+
116
+ // AND each unreadable image is replaced by a note instead of failing the
117
+ // whole request
118
+ expect(resolved.content[2]).toEqual({
119
+ type: "text",
120
+ text: UNSENDABLE_IMAGE_FORMAT_NOTE,
121
+ });
122
+ expect(resolved.content[3]).toEqual({
123
+ type: "text",
124
+ text: UNSENDABLE_IMAGE_FORMAT_NOTE,
125
+ });
126
+ });
127
+
128
+ test("keeps a HEIC photo for a provider that decodes HEIF", async () => {
129
+ /**
130
+ * Gemini reads image/heic and image/heif directly, so an untranscoded HEIC
131
+ * photo must reach it as an image rather than as an omission note.
132
+ */
133
+ // GIVEN a HEIC photo declared as a PNG, as an untranscoded upload arrives
134
+ const message = userMessage([inlineImage("image/png", HEIC_B64)]);
135
+
136
+ // WHEN a provider that accepts HEIF resolves it
137
+ const [resolved] = await resolveMediaReferences([message], {
138
+ acceptsHeif: true,
139
+ });
140
+
141
+ // THEN the image survives, declared as the HEIF type its brand names
142
+ expect(resolved.content[0]).toEqual(inlineImage("image/heic", HEIC_B64));
143
+ });
144
+
145
+ test("relabels an inline image whose declared type is not its actual type", async () => {
146
+ // GIVEN PNG bytes declared as JPEG, which every provider rejects outright
147
+ const message = userMessage([inlineImage("image/jpeg", PNG_B64)]);
148
+
149
+ // WHEN the send boundary resolves it
150
+ const [resolved] = await resolveMediaReferences([message]);
151
+
152
+ // THEN the declaration is corrected to what the bytes are, payload intact
153
+ expect(resolved.content[0]).toEqual(inlineImage("image/png", PNG_B64));
154
+ });
155
+
156
+ test("drops a stored image whose bytes are not a readable format", async () => {
157
+ // GIVEN a persisted attachment holding HEIC bytes under a .png name,
158
+ // referenced by a reloaded message
159
+ const block = await storedImageRef("photo.png", "image/png", HEIC_B64);
160
+
161
+ // WHEN the send boundary resolves the reference
162
+ const [resolved] = await resolveMediaReferences([userMessage([block])]);
163
+
164
+ // THEN it is replaced by the note rather than hydrated and sent
165
+ expect(resolved.content[0]).toEqual({
166
+ type: "text",
167
+ text: UNSENDABLE_IMAGE_FORMAT_NOTE,
168
+ });
169
+ });
170
+
171
+ test("keeps the unavailable-attachment note for a reference with no bytes", async () => {
172
+ // GIVEN a reference to an attachment row that does not exist
173
+ const block: ContentBlock = {
174
+ type: "image",
175
+ source: {
176
+ type: "workspace_ref",
177
+ media_type: "image/png",
178
+ attachmentId: "missing-attachment",
179
+ sizeBytes: 1024,
180
+ filename: "gone.png",
181
+ },
182
+ };
183
+
184
+ // WHEN the send boundary resolves it
185
+ const [resolved] = await resolveMediaReferences([userMessage([block])]);
186
+
187
+ // THEN an unreadable payload is reported as unavailable, not as a bad
188
+ // format: the file may simply have been collected
189
+ expect(resolved.content[0]).toEqual({
190
+ type: "text",
191
+ text: "[Attachment unavailable: image could not be loaded]",
192
+ });
193
+ });
194
+
195
+ test("validates images nested in a tool result", async () => {
196
+ // GIVEN a tool result carrying one readable and one corrupt image
197
+ const message = userMessage([
198
+ {
199
+ type: "tool_result",
200
+ tool_use_id: "tool-1",
201
+ content: "two screenshots",
202
+ contentBlocks: [
203
+ inlineImage("image/png", PNG_B64),
204
+ inlineImage("image/png", CORRUPT_PNG_B64),
205
+ ],
206
+ },
207
+ ]);
208
+
209
+ // WHEN the send boundary resolves it
210
+ const [resolved] = await resolveMediaReferences([message]);
211
+
212
+ // THEN the tool_use/tool_result pairing survives with the bad image noted
213
+ const toolResult = resolved.content[0];
214
+ if (toolResult.type !== "tool_result") {
215
+ throw new Error(`expected tool_result, got ${toolResult.type}`);
216
+ }
217
+ expect(toolResult.contentBlocks).toEqual([
218
+ inlineImage("image/png", PNG_B64),
219
+ { type: "text", text: UNSENDABLE_IMAGE_FORMAT_NOTE },
220
+ ]);
221
+ });
222
+ });
223
+
224
+ describe("isUnsendableImageSource", () => {
225
+ beforeEach(resetTables);
226
+
227
+ test("reports a stored attachment the send boundary will drop", async () => {
228
+ // GIVEN one attachment holding a real PNG and one holding HEIC bytes
229
+ const good = await storedImageRef("good.png", "image/png", PNG_B64);
230
+ const bad = await storedImageRef("bad.png", "image/png", HEIC_B64);
231
+ if (good.type !== "image" || bad.type !== "image") {
232
+ throw new Error("expected image blocks");
233
+ }
234
+
235
+ // WHEN each source is checked
236
+ // THEN only the unreadable one is reported, matching what the boundary does
237
+ expect(isUnsendableImageSource(good.source)).toBe(false);
238
+ expect(isUnsendableImageSource(bad.source)).toBe(true);
239
+ });
240
+
241
+ test("does not report a reference whose attachment is gone", () => {
242
+ // GIVEN a reference with no row behind it
243
+ // WHEN it is checked
244
+ // THEN it is not called unsendable: a missing payload is a separate
245
+ // failure, and naming it in the drop card would misinform the user
246
+ expect(
247
+ isUnsendableImageSource({
248
+ type: "workspace_ref",
249
+ media_type: "image/png",
250
+ attachmentId: "missing-attachment",
251
+ sizeBytes: 10,
252
+ filename: "gone.png",
253
+ }),
254
+ ).toBe(false);
255
+ });
256
+ });
257
+
258
+ describe("unsendable-image notice", () => {
259
+ beforeEach(resetTables);
260
+
261
+ test("names only the attachments the model cannot see", async () => {
262
+ // GIVEN a message whose attachments are one valid PNG and one HEIC-as-PNG
263
+ const msgConv = createConversation();
264
+ await addMessage(msgConv.id, "user", "look at these");
265
+ const good = await storedImageRef("holiday.png", "image/png", PNG_B64);
266
+ const bad = await storedImageRef("beach.png", "image/png", HEIC_B64);
267
+ if (good.type !== "image" || bad.type !== "image") {
268
+ throw new Error("expected image blocks");
269
+ }
270
+
271
+ // WHEN the notice decides what to name
272
+ const filenames = unsendableImageFilenames([
273
+ { filename: "holiday.png", source: good.source },
274
+ { filename: "beach.png", source: bad.source },
275
+ ]);
276
+
277
+ // THEN only the dropped file is named, so the user knows which to convert
278
+ expect(filenames).toEqual(["beach.png"]);
279
+
280
+ // AND the card copy names it and says what to do about it
281
+ expect(unsendableImageCardText(filenames)).toBe(
282
+ "beach.png was not sent to the model: the file is not a PNG, JPEG, GIF, or WebP image. Convert it and attach it again to include it.",
283
+ );
284
+ });
285
+
286
+ test("posts no notice when every attachment is readable", async () => {
287
+ // GIVEN attachments that are all valid images
288
+ const good = await storedImageRef("one.png", "image/png", PNG_B64);
289
+ if (good.type !== "image") {
290
+ throw new Error("expected an image block");
291
+ }
292
+
293
+ // WHEN the notice decides what to name
294
+ // THEN there is nothing to say, so no card is written
295
+ expect(
296
+ unsendableImageFilenames([{ filename: "one.png", source: good.source }]),
297
+ ).toEqual([]);
298
+ });
299
+
300
+ test("names every dropped file when a batch has several", () => {
301
+ // GIVEN two inline attachments whose bytes are unreadable
302
+ // WHEN the card copy is built for them
303
+ const text = unsendableImageCardText(["a.png", "b.png"]);
304
+
305
+ // THEN both are named in plural copy
306
+ expect(text).toBe(
307
+ "a.png, b.png were not sent to the model: the files are not PNG, JPEG, GIF, or WebP images. Convert them and attach them again to include them.",
308
+ );
309
+ });
310
+ });
@@ -38,6 +38,7 @@ import {
38
38
  reconcilePluginSourcesNow,
39
39
  resetPluginCacheForTests,
40
40
  } from "../plugins/mtime-cache.js";
41
+ import { resetPluginRegistryForTests } from "../plugins/registry.js";
41
42
  import { getSourceVersionsPath } from "../plugins/source-versions.js";
42
43
  import {
43
44
  __resetRegistryForTesting,
@@ -157,6 +158,7 @@ beforeEach(() => {
157
158
  // The registry pulls plugin tools via loadPluginTools(); clear its side
158
159
  // (tools + pulled fingerprints) so registrations never leak across tests.
159
160
  __resetRegistryForTesting();
161
+ resetPluginRegistryForTests();
160
162
  });
161
163
 
162
164
  afterAll(() => {
@@ -1,4 +1,28 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ── Mocks: declared before imports that depend on them ──────────────
4
+
5
+ let updateMessageContentShouldThrow = false;
6
+
7
+ const updateMessageContentMock = mock(
8
+ (_messageId: string, _content: string) => {
9
+ if (updateMessageContentShouldThrow) {
10
+ throw new Error("DB write failed");
11
+ }
12
+ },
13
+ );
14
+
15
+ mock.module("../persistence/conversation-crud.js", () => ({
16
+ updateMessageContent: updateMessageContentMock,
17
+ }));
18
+
19
+ const messagesInvalidated: string[] = [];
20
+
21
+ mock.module("../runtime/sync/resource-sync-events.js", () => ({
22
+ publishConversationMessagesChanged: (conversationId: string) => {
23
+ messagesInvalidated.push(conversationId);
24
+ },
25
+ }));
2
26
 
3
27
  import type { AssistantEvent } from "../api/index.js";
4
28
  import { VellumAdapter } from "../notifications/adapters/macos.js";
@@ -156,3 +180,102 @@ describe("VellumAdapter remotePushDispatched pass-through", () => {
156
180
  expect(intent.remotePushDispatched).toBeUndefined();
157
181
  });
158
182
  });
183
+
184
+ describe("VellumAdapter update", () => {
185
+ beforeEach(() => {
186
+ updateMessageContentMock.mockClear();
187
+ updateMessageContentShouldThrow = false;
188
+ messagesInvalidated.length = 0;
189
+ });
190
+
191
+ test("rewrites the persisted conversation message with the new body", async () => {
192
+ const { adapter } = captureBroadcast();
193
+
194
+ const result = await adapter.update!(
195
+ {
196
+ deliveryId: "delivery-1",
197
+ destination: "vellum",
198
+ messageId: "msg-1",
199
+ conversationId: "conv-1",
200
+ },
201
+ { title: "Daily Briefing", body: "Revised briefing text." },
202
+ );
203
+
204
+ expect(result.success).toBe(true);
205
+ expect(result.messageId).toBe("msg-1");
206
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
207
+ expect(updateMessageContentMock.mock.calls[0]).toEqual([
208
+ "msg-1",
209
+ "Revised briefing text.",
210
+ ]);
211
+ // A client holding the conversation open refetches on this tag alone.
212
+ expect(messagesInvalidated).toEqual(["conv-1"]);
213
+ });
214
+
215
+ test("rewrites without a conversation to invalidate", async () => {
216
+ // Deliveries recorded before the pairing carried a conversation have none
217
+ // to name, and the rewrite still stands on its own.
218
+ const { adapter } = captureBroadcast();
219
+
220
+ const result = await adapter.update!(
221
+ { deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
222
+ { body: "Revised briefing text." },
223
+ );
224
+
225
+ expect(result.success).toBe(true);
226
+ expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
227
+ expect(messagesInvalidated).toHaveLength(0);
228
+ });
229
+
230
+ test("leaves the message untouched on a title-only edit", async () => {
231
+ // The feed rewrites its summary only when the patch carries a body, so
232
+ // writing the title here would put the conversation out of step with the
233
+ // card it is supposed to match.
234
+ const { adapter } = captureBroadcast();
235
+
236
+ const result = await adapter.update!(
237
+ { deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
238
+ { title: "Daily Briefing" },
239
+ );
240
+
241
+ expect(result.success).toBe(true);
242
+ expect(updateMessageContentMock).not.toHaveBeenCalled();
243
+ });
244
+
245
+ test("writes an empty body through, matching the feed summary patch", async () => {
246
+ const { adapter } = captureBroadcast();
247
+
248
+ await adapter.update!(
249
+ { deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
250
+ { title: "Daily Briefing", body: "" },
251
+ );
252
+
253
+ expect(updateMessageContentMock.mock.calls[0]![1]).toBe("");
254
+ });
255
+
256
+ test("skips deliveries that persisted no conversation message", async () => {
257
+ const { adapter } = captureBroadcast();
258
+
259
+ const result = await adapter.update!(
260
+ { deliveryId: "delivery-1", destination: "vellum", messageId: null },
261
+ { body: "Revised briefing text." },
262
+ );
263
+
264
+ expect(result.success).toBe(false);
265
+ expect(result.error).toContain("missing_message_id");
266
+ expect(updateMessageContentMock).not.toHaveBeenCalled();
267
+ });
268
+
269
+ test("reports failure without throwing when the write fails", async () => {
270
+ const { adapter } = captureBroadcast();
271
+ updateMessageContentShouldThrow = true;
272
+
273
+ const result = await adapter.update!(
274
+ { deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
275
+ { body: "Revised briefing text." },
276
+ );
277
+
278
+ expect(result.success).toBe(false);
279
+ expect(result.error).toBe("DB write failed");
280
+ });
281
+ });
@@ -348,6 +348,28 @@ describe("OpenAIProvider", () => {
348
348
  }
349
349
  });
350
350
 
351
+ test("ollama wrapper prefers an explicit baseURL over OLLAMA_BASE_URL", () => {
352
+ const previousBaseUrl = process.env.OLLAMA_BASE_URL;
353
+ try {
354
+ process.env.OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1";
355
+ const ollama = new OllamaProvider("llama3.2", {
356
+ baseURL: "http://192.168.1.50:11434/v1",
357
+ });
358
+ expect(ollama.name).toBe("ollama");
359
+ expectOpenAIConstructorOptions({
360
+ apiKey: "ollama",
361
+ baseURL: "http://192.168.1.50:11434/v1",
362
+ timeout: DEFAULT_SDK_TIMEOUT_MS,
363
+ });
364
+ } finally {
365
+ if (previousBaseUrl !== undefined) {
366
+ process.env.OLLAMA_BASE_URL = previousBaseUrl;
367
+ } else {
368
+ delete process.env.OLLAMA_BASE_URL;
369
+ }
370
+ }
371
+ });
372
+
351
373
  test("ollama wrapper treats empty OLLAMA_BASE_URL as unset", () => {
352
374
  const previousBaseUrl = process.env.OLLAMA_BASE_URL;
353
375
  try {
@@ -14,7 +14,10 @@
14
14
  */
15
15
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
16
16
 
17
- import { isPersonalMemoryAllowed } from "../daemon/trust-context.js";
17
+ import {
18
+ FALLBACK_TURN_TRUST,
19
+ isPersonalMemoryAllowed,
20
+ } from "../daemon/trust-context.js";
18
21
  import type { TrustContext } from "../daemon/trust-context-types.js";
19
22
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
20
23
 
@@ -45,16 +48,29 @@ describe("isPersonalMemoryAllowed — no exposure to non-guardian channel actors
45
48
  expect(isPersonalMemoryAllowed(slack("unverified_contact"))).toBe(false);
46
49
  });
47
50
 
48
- test("allows personal memory for the guardian and for local / internal turns", () => {
51
+ test("allows personal memory for the guardian and for local / native turns", () => {
49
52
  expect(isPersonalMemoryAllowed(slack("guardian"))).toBe(true);
50
- // Local/native turn — no resolved actor.
53
+ // Local/native turn: no actor bound at all. `resolveTrustClass` elevates
54
+ // this to guardian under a disabled-auth posture, so identity resolution
55
+ // admits it rather than the memory gate carving out an exception.
51
56
  expect(isPersonalMemoryAllowed(undefined)).toBe(true);
52
- // Internal vellum-channel flow.
57
+ });
58
+
59
+ test("blocks personal memory for the fail-closed fallback trust", () => {
60
+ // `{vellum, unknown}` is FALLBACK_TURN_TRUST, not an internal flow: it is
61
+ // what the daemon substitutes when no per-turn actor was captured. Its own
62
+ // contract is to bias to `unknown` "so a missing snapshot cannot
63
+ // accidentally grant elevated trust", and every other consumer treats it
64
+ // as low-trust and never guardian. Personal memory is elevated trust, so
65
+ // the gate has to agree with them.
66
+ expect(isPersonalMemoryAllowed(FALLBACK_TURN_TRUST)).toBe(false);
67
+ // And a resolved non-guardian is denied on the console for the same
68
+ // reason: a surface is not an actor.
53
69
  expect(
54
70
  isPersonalMemoryAllowed({
55
71
  sourceChannel: "vellum",
56
- trustClass: "unknown",
72
+ trustClass: "unverified_contact",
57
73
  }),
58
- ).toBe(true);
74
+ ).toBe(false);
59
75
  });
60
76
  });
@@ -44,12 +44,8 @@ mock.module("../permissions/checker.js", () => ({
44
44
  }
45
45
  return { decision: "allow", reason: "allowed" };
46
46
  },
47
- generateAllowlistOptions: () => [
48
- { label: "exact", description: "exact", pattern: "exact" },
49
- ],
50
47
  generateScopeOptions: () =>
51
48
  scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
52
- getCachedAssessment: () => undefined,
53
49
  }));
54
50
 
55
51
  // Mock every export so downstream test files that dynamically import modules
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { afterEach, describe, expect, test } from "bun:test";
4
+ import { afterAll, afterEach, describe, expect, test } from "bun:test";
5
5
 
6
6
  import {
7
7
  ensureDataDir,
@@ -23,6 +23,21 @@ import {
23
23
  } from "../util/platform.js";
24
24
 
25
25
  const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
26
+
27
+ // This file characterizes path resolution itself (including the ~/.vellum
28
+ // fallback and literal override paths) without reading or writing those
29
+ // locations, so the live-workspace guard is deliberately bypassed.
30
+ const originalAllowRealWorkspace =
31
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
32
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
33
+ afterAll(() => {
34
+ if (originalAllowRealWorkspace === undefined) {
35
+ delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
36
+ } else {
37
+ process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS =
38
+ originalAllowRealWorkspace;
39
+ }
40
+ });
26
41
  const originalVellumEnvironment = process.env.VELLUM_ENVIRONMENT;
27
42
  const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
28
43
 
@@ -102,21 +102,28 @@ describe("resolveCredential", () => {
102
102
  });
103
103
 
104
104
  describe("plugin scoping", () => {
105
- test("allows a plugin to resolve a credential whose field matches its name", async () => {
106
- seedCredential("openai", "acme", "scoped-secret");
107
- const value = await runInPluginContext("acme", () =>
108
- resolveCredential("openai/acme"),
105
+ test("allows a plugin to resolve a credential whose service matches its name", async () => {
106
+ seedCredential("imessage", "api_key", "comms-key");
107
+ const value = await runInPluginContext("imessage", () =>
108
+ resolveCredential("imessage/api_key"),
109
109
  );
110
- expect(value).toBe("scoped-secret");
110
+ expect(value).toBe("comms-key");
111
111
  });
112
112
 
113
- test("blocks a plugin from resolving a credential whose field differs from its name", async () => {
113
+ test("blocks a plugin from resolving another service", async () => {
114
114
  seedCredential("openai", "api_key", "sk-secret");
115
115
  await expect(
116
116
  runInPluginContext("acme", () => resolveCredential("openai/api_key")),
117
117
  ).rejects.toThrow(/out of scope/);
118
118
  });
119
119
 
120
+ test("blocks a plugin from resolving another service even when the field matches its name", async () => {
121
+ seedCredential("openai", "acme", "scoped-secret");
122
+ await expect(
123
+ runInPluginContext("acme", () => resolveCredential("openai/acme")),
124
+ ).rejects.toThrow(/out of scope/);
125
+ });
126
+
120
127
  test("does not read the secure backend when out of scope", async () => {
121
128
  seedCredential("openai", "api_key", "sk-secret");
122
129
  getSpy.mockClear();
@@ -126,15 +133,17 @@ describe("resolveCredential", () => {
126
133
  expect(getSpy).not.toHaveBeenCalled();
127
134
  });
128
135
 
129
- test("scoping applies by field only, across any service", async () => {
130
- seedCredential("stripe", "acme", "stripe-secret");
131
- seedCredential("openai", "acme", "openai-secret");
136
+ test("scoping applies to every field under the plugin's service", async () => {
137
+ seedCredential("imessage", "api_key", "comms-key");
138
+ seedCredential("imessage", "photon_project_id", "proj-id");
132
139
  await expect(
133
- runInPluginContext("acme", () => resolveCredential("stripe/acme")),
134
- ).resolves.toBe("stripe-secret");
140
+ runInPluginContext("imessage", () => resolveCredential("imessage/api_key")),
141
+ ).resolves.toBe("comms-key");
135
142
  await expect(
136
- runInPluginContext("acme", () => resolveCredential("openai/acme")),
137
- ).resolves.toBe("openai-secret");
143
+ runInPluginContext("imessage", () =>
144
+ resolveCredential("imessage/photon_project_id"),
145
+ ),
146
+ ).resolves.toBe("proj-id");
138
147
  });
139
148
  });
140
149
  });
@@ -136,9 +136,14 @@ describe("ensurePluginApiShim", () => {
136
136
  `}));\n`,
137
137
  );
138
138
 
139
+ // Explicit env spread: under bun test, a child's default env reflects the
140
+ // process-start environ, not later process.env mutations. Without this
141
+ // the child misses the preload's VELLUM_WORKSPACE_DIR and resolves the
142
+ // real ~/.vellum/workspace (refused by the live-workspace guard).
139
143
  const result = spawnSync(process.execPath, ["run", probe], {
140
144
  encoding: "utf8",
141
145
  timeout: 30_000,
146
+ env: { ...process.env },
142
147
  });
143
148
  expect(result.status).toBe(0);
144
149
  const lastLine = result.stdout.trim().split("\n").at(-1) ?? "";