@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Unit tests for the contacts read API.
3
+ *
4
+ * `handleListContacts`, `handleGetContact`, and the `search_contacts`
5
+ * no-filter case relay to the gateway rich read (`contacts_list_rich` /
6
+ * `contacts_get_rich`), which is the source of truth for the ACL fields
7
+ * (`role`/`status`/`policy`/`verifiedAt`/`interactionCount`/`lastInteraction`).
8
+ * These tests assert the serialized response shape is gateway-sourced and
9
+ * unchanged for the web client, that no read path falls back to the assistant
10
+ * DB, and that a relay failure fails closed (surfaces an error) instead of
11
+ * reading local ACL.
12
+ */
13
+
14
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
15
+
16
+ import { IpcCallError } from "@vellumai/gateway-client/ipc-client";
17
+
18
+ let ipcCalls: { method: string; params?: Record<string, unknown> }[] = [];
19
+ let ipcResult: unknown = {};
20
+ let ipcError: Error | undefined;
21
+
22
+ const ipcCallPersistentMock = mock(
23
+ async (method: string, params?: Record<string, unknown>) => {
24
+ ipcCalls.push({ method, params });
25
+ if (ipcError) throw ipcError;
26
+ return ipcResult;
27
+ },
28
+ );
29
+
30
+ const actualGatewayClient = await import("../../../ipc/gateway-client.js");
31
+
32
+ mock.module("../../../ipc/gateway-client.js", () => ({
33
+ ...actualGatewayClient,
34
+ ipcCallPersistent: ipcCallPersistentMock,
35
+ }));
36
+
37
+ // Guard: fail loudly if any read path falls back to the assistant DB for ACL
38
+ // fields. The relay read paths must source role/status/stats from the gateway.
39
+ const actualContactStore = await import("../../../contacts/contact-store.js");
40
+
41
+ const contactStoreReadGuard = mock(() => {
42
+ throw new Error(
43
+ "assistant contact-store read must not happen on the gateway relay path",
44
+ );
45
+ });
46
+
47
+ mock.module("../../../contacts/contact-store.js", () => ({
48
+ ...actualContactStore,
49
+ getContact: contactStoreReadGuard,
50
+ listContacts: contactStoreReadGuard,
51
+ getAssistantContactMetadata: contactStoreReadGuard,
52
+ }));
53
+
54
+ const { handleListContacts, handleGetContact, ROUTES } = await import(
55
+ "../contact-routes.js"
56
+ );
57
+
58
+ const gatewayChannel = {
59
+ id: "ch_1",
60
+ contactId: "ct_1",
61
+ type: "sms",
62
+ address: "+15550100",
63
+ isPrimary: true,
64
+ externalUserId: null,
65
+ status: "active",
66
+ policy: "allow",
67
+ verifiedAt: 1700,
68
+ verifiedVia: "invite",
69
+ lastSeenAt: 1800,
70
+ interactionCount: 7,
71
+ lastInteraction: 1900,
72
+ revokedReason: null,
73
+ blockedReason: null,
74
+ };
75
+
76
+ const gatewayContact = {
77
+ id: "ct_1",
78
+ displayName: "Alice",
79
+ role: "member",
80
+ notes: "a note",
81
+ contactType: "human",
82
+ lastInteraction: 1900,
83
+ interactionCount: 7,
84
+ createdAt: 1000,
85
+ updatedAt: 1500,
86
+ channels: [gatewayChannel],
87
+ };
88
+
89
+ describe("contacts read API relays from the gateway", () => {
90
+ beforeEach(() => {
91
+ ipcCalls = [];
92
+ ipcResult = {};
93
+ ipcError = undefined;
94
+ ipcCallPersistentMock.mockClear();
95
+ contactStoreReadGuard.mockClear();
96
+ });
97
+
98
+ test("list relays to contacts_list_rich and serializes the gateway ACL fields", async () => {
99
+ ipcResult = { ok: true, contacts: [gatewayContact] };
100
+
101
+ const result = await handleListContacts({ limit: "50" });
102
+
103
+ expect(ipcCalls).toEqual([
104
+ { method: "contacts_list_rich", params: { limit: 50 } },
105
+ ]);
106
+ expect(result.ok).toBe(true);
107
+ expect(result.contacts).toHaveLength(1);
108
+
109
+ const [contact] = result.contacts;
110
+ // ACL fields are gateway-sourced and reach the web client unchanged.
111
+ expect(contact.role).toBe("member");
112
+ expect(contact.interactionCount).toBe(7);
113
+ expect(contact.lastInteraction).toBe(1900);
114
+ const channel = contact.channels[0] as Record<string, unknown>;
115
+ expect(channel.status).toBe("active");
116
+ expect(channel.policy).toBe("allow");
117
+ expect(channel.verifiedAt).toBe(1700);
118
+ expect(channel.interactionCount).toBe(7);
119
+ expect(channel.lastInteraction).toBe(1900);
120
+ // Channel compat echoes address into externalUserId for older clients.
121
+ expect(channel.externalUserId).toBe("+15550100");
122
+
123
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
124
+ });
125
+
126
+ test("list forwards the role filter to the gateway", async () => {
127
+ ipcResult = { ok: true, contacts: [] };
128
+
129
+ await handleListContacts({ limit: "10", role: "guardian" });
130
+
131
+ expect(ipcCalls).toEqual([
132
+ {
133
+ method: "contacts_list_rich",
134
+ params: { limit: 10, role: "guardian" },
135
+ },
136
+ ]);
137
+ });
138
+
139
+ test("list fails closed when the gateway relay is unavailable", async () => {
140
+ ipcError = new IpcCallError("gateway down", {
141
+ statusCode: 503,
142
+ errorCode: "UNAVAILABLE",
143
+ });
144
+
145
+ await expect(handleListContacts({ limit: "50" })).rejects.toMatchObject({
146
+ message: "gateway down",
147
+ statusCode: 503,
148
+ });
149
+ // No assistant-DB fallback read.
150
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
151
+ });
152
+
153
+ test("get relays to contacts_get_rich and serializes the gateway ACL fields", async () => {
154
+ ipcResult = { ok: true, contact: gatewayContact };
155
+
156
+ const result = await handleGetContact("ct_1");
157
+
158
+ expect(ipcCalls).toEqual([
159
+ { method: "contacts_get_rich", params: { contactId: "ct_1" } },
160
+ ]);
161
+ expect(result.ok).toBe(true);
162
+ expect(result.contact.role).toBe("member");
163
+ expect(result.contact.interactionCount).toBe(7);
164
+ const channel = result.contact.channels[0] as Record<string, unknown>;
165
+ expect(channel.status).toBe("active");
166
+ expect(channel.externalUserId).toBe("+15550100");
167
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
168
+ });
169
+
170
+ test("get surfaces a clean gateway not-found as a 404", async () => {
171
+ ipcResult = { ok: true };
172
+
173
+ await expect(handleGetContact("missing")).rejects.toMatchObject({
174
+ statusCode: 404,
175
+ });
176
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
177
+ });
178
+
179
+ test("get fails closed on a relay failure (no assistant-DB fallback)", async () => {
180
+ ipcError = new IpcCallError("gateway down", {
181
+ statusCode: 503,
182
+ errorCode: "UNAVAILABLE",
183
+ });
184
+
185
+ await expect(handleGetContact("ct_1")).rejects.toMatchObject({
186
+ message: "gateway down",
187
+ statusCode: 503,
188
+ });
189
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
190
+ });
191
+
192
+ test("search no-filter relays to the gateway list read", async () => {
193
+ ipcResult = { ok: true, contacts: [gatewayContact] };
194
+
195
+ const route = ROUTES.find((r) => r.operationId === "search_contacts");
196
+ expect(route).toBeDefined();
197
+
198
+ const contacts = (await route!.handler({ body: {} })) as Array<{
199
+ role: string;
200
+ interactionCount: number;
201
+ channels: { status: string }[];
202
+ }>;
203
+
204
+ expect(ipcCalls).toEqual([
205
+ { method: "contacts_list_rich", params: { limit: 50 } },
206
+ ]);
207
+ expect(contacts[0].role).toBe("member");
208
+ expect(contacts[0].interactionCount).toBe(7);
209
+ expect(contacts[0].channels[0].status).toBe("active");
210
+ expect(contactStoreReadGuard).not.toHaveBeenCalled();
211
+ });
212
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Unit tests for the global-search contacts ordering source.
3
+ *
4
+ * Recency ordering for contacts surfaces `contacts.updatedAt`, never the
5
+ * channel-derived `lastInteraction` column. Daemon-native contact search
6
+ * carries no gateway-relayed ContactRead, so `updatedAt` is the deterministic
7
+ * ordering key.
8
+ */
9
+
10
+ import { afterEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ import type { ContactWithChannels } from "../../../contacts/types.js";
13
+
14
+ let searchContactsResult: ContactWithChannels[] = [];
15
+
16
+ const searchContactsMock = mock(() => searchContactsResult);
17
+
18
+ const actualContactStore = await import("../../../contacts/contact-store.js");
19
+
20
+ mock.module("../../../contacts/contact-store.js", () => ({
21
+ ...actualContactStore,
22
+ searchContacts: searchContactsMock,
23
+ }));
24
+
25
+ const { ROUTES } = await import("../global-search-routes.js");
26
+
27
+ const handler = ROUTES.find((r) => r.operationId === "search_global")!.handler;
28
+
29
+ function makeContact(
30
+ overrides: Partial<ContactWithChannels>,
31
+ ): ContactWithChannels {
32
+ return {
33
+ id: "ct_1",
34
+ displayName: "Alice",
35
+ notes: null,
36
+ lastInteraction: null,
37
+ interactionCount: 0,
38
+ createdAt: 1,
39
+ updatedAt: 1,
40
+ role: "contact",
41
+ contactType: "human",
42
+ principalId: null,
43
+ userFile: null,
44
+ channels: [],
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ afterEach(() => {
50
+ searchContactsResult = [];
51
+ searchContactsMock.mockClear();
52
+ });
53
+
54
+ describe("global-search contacts recency source", () => {
55
+ test("surfaces contacts.updatedAt as lastInteraction, not the channel column", async () => {
56
+ searchContactsResult = [
57
+ makeContact({
58
+ id: "ct_1",
59
+ displayName: "Alice",
60
+ updatedAt: 5000,
61
+ lastInteraction: 9999,
62
+ }),
63
+ ];
64
+
65
+ const result = (await handler({
66
+ queryParams: { q: "ali", categories: "contacts" },
67
+ })) as { results: { contacts: { id: string; lastInteraction: number }[] } };
68
+
69
+ expect(result.results.contacts).toHaveLength(1);
70
+ expect(result.results.contacts[0].lastInteraction).toBe(5000);
71
+ });
72
+
73
+ test("preserves searchContacts ordering deterministically", async () => {
74
+ searchContactsResult = [
75
+ makeContact({ id: "ct_a", displayName: "A", updatedAt: 300 }),
76
+ makeContact({ id: "ct_b", displayName: "B", updatedAt: 200 }),
77
+ makeContact({ id: "ct_c", displayName: "C", updatedAt: 100 }),
78
+ ];
79
+
80
+ const result = (await handler({
81
+ queryParams: { q: "x", categories: "contacts" },
82
+ })) as { results: { contacts: { id: string; lastInteraction: number }[] } };
83
+
84
+ expect(result.results.contacts.map((c) => c.id)).toEqual([
85
+ "ct_a",
86
+ "ct_b",
87
+ "ct_c",
88
+ ]);
89
+ expect(result.results.contacts.map((c) => c.lastInteraction)).toEqual([
90
+ 300, 200, 100,
91
+ ]);
92
+ });
93
+ });
@@ -25,10 +25,12 @@ interface StubConversation {
25
25
  handleSurfaceActionThrows?: Error;
26
26
  handleSurfaceUndoCalled?: boolean;
27
27
  handleSurfaceUndoThrows?: Error;
28
+ trustContext?: { trustClass: string; sourceChannel: string };
28
29
  surfaceActionCalls: Array<{
29
30
  surfaceId: string;
30
31
  actionId: string;
31
32
  data: unknown;
33
+ sourceActorPrincipalId?: string;
32
34
  }>;
33
35
  }
34
36
 
@@ -42,6 +44,47 @@ const findBySurfaceCalls: string[] = [];
42
44
  const getOrCreateCalls: string[] = [];
43
45
  const rawGetCalls: Array<{ sql: string; params: unknown[] }> = [];
44
46
 
47
+ // Gateway guardian-delivery list (shared by the route's dev-bypass lookup and
48
+ // the local-principal-trust mapper): null = unreachable, [] = no guardian.
49
+ let mockGuardianList: Array<Record<string, unknown>> | null = [];
50
+ let httpAuthDisabled = false;
51
+
52
+ // Stub for the shared reset-drift helper. The route under test only consumes
53
+ // its result (a guardian TrustContext or null); the gate itself is covered in
54
+ // runtime/__tests__/guardian-vellum-migration.test.ts. Tests set
55
+ // `mockReResolve` per case and read `reResolveCalls` to assert routing.
56
+ const reResolveCalls: string[] = [];
57
+ let mockReResolve: { trustClass: string; sourceChannel: string } | null = null;
58
+
59
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
60
+ getGuardianDelivery: (_input?: { channelTypes?: string[] }) =>
61
+ Promise.resolve(mockGuardianList),
62
+ peekCachedGuardianDelivery: () => mockGuardianList ?? undefined,
63
+ guardianForChannel: (
64
+ list: Array<Record<string, unknown>>,
65
+ channelType: string,
66
+ ) => list.find((g) => g.channelType === channelType && g.status === "active"),
67
+ }));
68
+
69
+ mock.module("../../../contacts/contact-store.js", () => ({
70
+ findGuardianForChannel: (_channelType: string) => null,
71
+ findContactByAddress: () => null,
72
+ }));
73
+
74
+ mock.module("../../../config/env.js", () => ({
75
+ isHttpAuthDisabled: () => httpAuthDisabled,
76
+ }));
77
+
78
+ mock.module("../../guardian-vellum-migration.js", () => ({
79
+ reResolveTrustOnResetDrift: async (
80
+ incomingPrincipalId: string,
81
+ _sourceChannel: string,
82
+ ) => {
83
+ reResolveCalls.push(incomingPrincipalId);
84
+ return mockReResolve;
85
+ },
86
+ }));
87
+
45
88
  mock.module("../../../daemon/conversation-registry.js", () => ({
46
89
  findConversation: (id: string) => {
47
90
  findConvCalls.push(id);
@@ -106,8 +149,14 @@ function makeStub(id: string): StubConversation {
106
149
  surfaceId: string,
107
150
  actionId: string,
108
151
  data: unknown,
152
+ sourceActorPrincipalId?: string,
109
153
  ) => {
110
- stub.surfaceActionCalls.push({ surfaceId, actionId, data });
154
+ stub.surfaceActionCalls.push({
155
+ surfaceId,
156
+ actionId,
157
+ data,
158
+ sourceActorPrincipalId,
159
+ });
111
160
  if (stub.handleSurfaceActionThrows) throw stub.handleSurfaceActionThrows;
112
161
  return stub.handleSurfaceActionResult;
113
162
  },
@@ -115,6 +164,9 @@ function makeStub(id: string): StubConversation {
115
164
  stub.handleSurfaceUndoCalled = true;
116
165
  if (stub.handleSurfaceUndoThrows) throw stub.handleSurfaceUndoThrows;
117
166
  },
167
+ setTrustContext: (ctx: { trustClass: string; sourceChannel: string }) => {
168
+ stub.trustContext = ctx;
169
+ },
118
170
  });
119
171
  return stub;
120
172
  }
@@ -132,6 +184,10 @@ beforeEach(() => {
132
184
  findBySurfaceCalls.length = 0;
133
185
  getOrCreateCalls.length = 0;
134
186
  rawGetCalls.length = 0;
187
+ mockGuardianList = [];
188
+ httpAuthDisabled = false;
189
+ reResolveCalls.length = 0;
190
+ mockReResolve = null;
135
191
  });
136
192
 
137
193
  // ---------------------------------------------------------------------------
@@ -310,6 +366,50 @@ describe("triggerSurfaceAction handler", () => {
310
366
  expect(rawGetCalls[0]!.params).toEqual([`%"surfaceId":"\\%"%`]);
311
367
  });
312
368
 
369
+ test("threads x-vellum-actor-principal-id into handleSurfaceAction", async () => {
370
+ const live = makeStub("conv-principal");
371
+ memoryBySurface = live;
372
+
373
+ const handler = findHandler("triggerSurfaceAction");
374
+ await handler({
375
+ body: { surfaceId: "surf-p", actionId: "act-p" },
376
+ headers: { "x-vellum-actor-principal-id": "principal-committer" },
377
+ });
378
+
379
+ expect(live.surfaceActionCalls).toEqual([
380
+ {
381
+ surfaceId: "surf-p",
382
+ actionId: "act-p",
383
+ data: undefined,
384
+ sourceActorPrincipalId: "principal-committer",
385
+ },
386
+ ]);
387
+ });
388
+
389
+ test("resolves dev-bypass to the guardian principal before threading the turn", async () => {
390
+ httpAuthDisabled = true;
391
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
392
+ const live = makeStub("conv-dev-thread");
393
+ memoryBySurface = live;
394
+
395
+ const handler = findHandler("triggerSurfaceAction");
396
+ await handler({
397
+ body: { surfaceId: "surf-dt", actionId: "act-dt" },
398
+ headers: { "x-vellum-actor-principal-id": "dev-bypass" },
399
+ });
400
+
401
+ // dev-bypass is translated so the surface turn matches the SSE host-proxy
402
+ // client's registered guardian principal (CU/app-control same-actor check).
403
+ expect(live.surfaceActionCalls).toEqual([
404
+ {
405
+ surfaceId: "surf-dt",
406
+ actionId: "act-dt",
407
+ data: undefined,
408
+ sourceActorPrincipalId: GUARDIAN_PRINCIPAL,
409
+ },
410
+ ]);
411
+ });
412
+
313
413
  test("propagates accepted=false rejection as BadRequestError", async () => {
314
414
  const live = makeStub("conv-reject");
315
415
  live.handleSurfaceActionResult = {
@@ -335,6 +435,120 @@ describe("triggerSurfaceAction handler", () => {
335
435
  });
336
436
  });
337
437
 
438
+ // ---------------------------------------------------------------------------
439
+ // Trust context resolution
440
+ // ---------------------------------------------------------------------------
441
+
442
+ const GUARDIAN_PRINCIPAL = "principal-guardian";
443
+ // Daemon-minted vellum-principal-* ids: the DB-reset drift signature. The
444
+ // gateway rebinds to a fresh id while the client still holds a JWT for the old.
445
+ const VELLUM_PRINCIPAL_OLD = "vellum-principal-old";
446
+ const VELLUM_PRINCIPAL_NEW = "vellum-principal-new";
447
+
448
+ function guardianDelivery(principalId: string): Record<string, unknown> {
449
+ return {
450
+ channelType: "vellum",
451
+ contactId: "contact-1",
452
+ principalId,
453
+ address: "guardian-address",
454
+ externalChatId: "guardian-chat",
455
+ status: "active",
456
+ };
457
+ }
458
+
459
+ // A guardian TrustContext the stubbed helper hands back on a recovered drift.
460
+ function guardianCtx(): { trustClass: string; sourceChannel: string } {
461
+ return { trustClass: "guardian", sourceChannel: "vellum" };
462
+ }
463
+
464
+ describe("triggerSurfaceAction trust context", () => {
465
+ test("guardian principal → guardian from the gateway binding, helper not called", async () => {
466
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
467
+ const live = makeStub("conv-guardian");
468
+ memoryBySurface = live;
469
+
470
+ const handler = findHandler("triggerSurfaceAction");
471
+ await handler({
472
+ body: { surfaceId: "surf-g", actionId: "act-g" },
473
+ headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
474
+ });
475
+
476
+ expect(live.trustContext?.trustClass).toBe("guardian");
477
+ expect(live.trustContext?.sourceChannel).toBe("vellum");
478
+ // First-pass resolve already granted guardian, so the drift helper is skipped.
479
+ expect(reResolveCalls).toEqual([]);
480
+ });
481
+
482
+ test("unknown principal: helper consulted, null result stays unknown (fail closed)", async () => {
483
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
484
+ mockReResolve = null;
485
+ const live = makeStub("conv-unknown");
486
+ memoryBySurface = live;
487
+
488
+ const handler = findHandler("triggerSurfaceAction");
489
+ await handler({
490
+ body: { surfaceId: "surf-u", actionId: "act-u" },
491
+ headers: { "x-vellum-actor-principal-id": VELLUM_PRINCIPAL_OLD },
492
+ });
493
+
494
+ expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
495
+ expect(live.trustContext?.trustClass).toBe("unknown");
496
+ });
497
+
498
+ test("reset drift: helper returns guardian → route adopts it", async () => {
499
+ mockGuardianList = [guardianDelivery(VELLUM_PRINCIPAL_NEW)];
500
+ mockReResolve = guardianCtx();
501
+ const live = makeStub("conv-drift");
502
+ memoryBySurface = live;
503
+
504
+ const handler = findHandler("triggerSurfaceAction");
505
+ await handler({
506
+ body: { surfaceId: "surf-d", actionId: "act-d" },
507
+ headers: { "x-vellum-actor-principal-id": VELLUM_PRINCIPAL_OLD },
508
+ });
509
+
510
+ expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
511
+ expect(live.trustContext?.trustClass).toBe("guardian");
512
+ });
513
+
514
+ test("dev-bypass resolves the real guardian principal from the gateway, helper not called", async () => {
515
+ httpAuthDisabled = true;
516
+ mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
517
+ const live = makeStub("conv-dev");
518
+ memoryBySurface = live;
519
+
520
+ const handler = findHandler("triggerSurfaceAction");
521
+ await handler({
522
+ body: { surfaceId: "surf-dev", actionId: "act-dev" },
523
+ headers: { "x-vellum-actor-principal-id": "dev-bypass" },
524
+ });
525
+
526
+ // The synthetic dev-bypass principal is translated to the real guardian,
527
+ // yielding a guardian trust context without consulting the drift helper.
528
+ expect(live.trustContext?.trustClass).toBe("guardian");
529
+ expect(reResolveCalls).toEqual([]);
530
+ });
531
+
532
+ test("dev-bypass with an empty gateway: helper null result → unknown (fail closed)", async () => {
533
+ httpAuthDisabled = true;
534
+ // The gateway has no active binding, so dev-bypass cannot translate to a
535
+ // real guardian; the first-pass resolve is unknown and the helper, returning
536
+ // null, leaves trust unknown.
537
+ mockGuardianList = [];
538
+ mockReResolve = null;
539
+ const live = makeStub("conv-dev-fallback");
540
+ memoryBySurface = live;
541
+
542
+ const handler = findHandler("triggerSurfaceAction");
543
+ await handler({
544
+ body: { surfaceId: "surf-devf", actionId: "act-devf" },
545
+ headers: { "x-vellum-actor-principal-id": "dev-bypass" },
546
+ });
547
+
548
+ expect(live.trustContext?.trustClass).toBe("unknown");
549
+ });
550
+ });
551
+
338
552
  // ---------------------------------------------------------------------------
339
553
  // undoSurfaceAction
340
554
  // ---------------------------------------------------------------------------
@@ -92,7 +92,7 @@ async function handleBrowserExecute({
92
92
  // register with (dev-bypass otherwise mismatches).
93
93
  const headerActor =
94
94
  headers["x-vellum-actor-principal-id"]?.trim() || undefined;
95
- const sourceActorPrincipalId = resolveActorPrincipalIdForLocalGuardian(
95
+ const sourceActorPrincipalId = await resolveActorPrincipalIdForLocalGuardian(
96
96
  conversation?.getTurnActorPrincipalId() ?? headerActor,
97
97
  );
98
98
 
@@ -179,7 +179,7 @@ export async function handleCreateVerificationSession({
179
179
  }
180
180
 
181
181
  // Inbound challenge path
182
- const result = createInboundChallenge(channel, rebind, conversationId);
182
+ const result = await createInboundChallenge(channel, rebind, conversationId);
183
183
  if (!result.success) {
184
184
  throw new BadRequestError(
185
185
  (result as { message?: string }).message ??
@@ -192,12 +192,12 @@ export async function handleCreateVerificationSession({
192
192
  /**
193
193
  * GET /v1/channel-verification-sessions/status
194
194
  */
195
- function handleGetVerificationStatus({
195
+ async function handleGetVerificationStatus({
196
196
  queryParams = {},
197
197
  body = {},
198
198
  }: RouteHandlerArgs) {
199
199
  const channel = (queryParams.channel ?? (body as Record<string, unknown>).channel) as ChannelId | undefined;
200
- return getVerificationStatus(channel);
200
+ return await getVerificationStatus(channel);
201
201
  }
202
202
 
203
203
  /**
@@ -17,8 +17,6 @@ import { IpcCallError } from "@vellumai/gateway-client/ipc-client";
17
17
  import { z } from "zod";
18
18
 
19
19
  import {
20
- getAssistantContactMetadata,
21
- getContact,
22
20
  listContacts,
23
21
  mergeContacts,
24
22
  searchContacts,
@@ -139,9 +137,10 @@ const contactSchema = z.object({
139
137
 
140
138
  /**
141
139
  * Relay a non-search contact list read to the gateway (source of truth for ACL
142
- * fields), falling back to the assistant DB on IPC failure. Shared by the GET
143
- * `contacts` list and the `search_contacts` no-filter case so both serve
144
- * gateway-sourced data consistently.
140
+ * fields). Shared by the GET `contacts` list and the `search_contacts`
141
+ * no-filter case so both serve gateway-sourced data consistently. Fail-closed:
142
+ * a relay failure surfaces as an error rather than reading ACL from the
143
+ * assistant DB.
145
144
  */
146
145
  async function relayListContacts(
147
146
  limit: number,
@@ -158,15 +157,7 @@ async function relayListContacts(
158
157
  contacts: contacts.map(prepareContactResponse),
159
158
  };
160
159
  } catch (err) {
161
- log.warn(
162
- { err },
163
- "relayListContacts: gateway relay failed; falling back to assistant DB",
164
- );
165
- const contacts = listContacts(limit, role);
166
- return {
167
- ok: true,
168
- contacts: contacts.map(prepareContactResponse),
169
- };
160
+ rethrowGatewayError(err);
170
161
  }
171
162
  }
172
163
 
@@ -240,25 +231,10 @@ export async function handleGetContact(contactId: string) {
240
231
  assistantMetadata: assistantMetadata ?? undefined,
241
232
  };
242
233
  } catch (err) {
243
- // A clean not-found is a real 404 don't fall back or mask it.
234
+ // A clean not-found is a real 404. Any other relay failure fails closed
235
+ // rather than reading ACL from the assistant DB.
244
236
  if (err instanceof NotFoundError) throw err;
245
- log.warn(
246
- { err, contactId },
247
- "handleGetContact: gateway relay failed; falling back to assistant DB",
248
- );
249
- const contact = getContact(contactId);
250
- if (!contact) {
251
- throw new NotFoundError(`Contact "${contactId}" not found`);
252
- }
253
- const assistantMeta =
254
- contact.contactType === "assistant"
255
- ? getAssistantContactMetadata(contact.id)
256
- : undefined;
257
- return {
258
- ok: true,
259
- contact: prepareContactResponse(contact),
260
- assistantMetadata: assistantMeta ?? undefined,
261
- };
237
+ rethrowGatewayError(err);
262
238
  }
263
239
  }
264
240