@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
@@ -7,11 +7,22 @@
7
7
  * assistant-side since it reacts to incoming JWT principals.
8
8
  */
9
9
 
10
+ import type { ChannelId } from "../channels/types.js";
10
11
  import {
11
12
  findGuardianForChannel,
12
13
  updateContactPrincipalAndChannel,
13
14
  } from "../contacts/contact-store.js";
15
+ import {
16
+ getGuardianDelivery,
17
+ guardianForChannel,
18
+ } from "../contacts/guardian-delivery-reader.js";
19
+ import type { TrustContext } from "../daemon/trust-context.js";
14
20
  import { getLogger } from "../util/logger.js";
21
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
22
+ import {
23
+ resolveTrustContext,
24
+ withSourceChannel,
25
+ } from "./trust-context-resolver.js";
15
26
 
16
27
  const log = getLogger("guardian-vellum-migration");
17
28
 
@@ -31,18 +42,37 @@ const log = getLogger("guardian-vellum-migration");
31
42
  * minted by this daemon's signing key.
32
43
  *
33
44
  * Returns true if healing occurred, false otherwise.
45
+ *
46
+ * The gateway binding supplies the authoritative principal; the local
47
+ * assistant-mirror row is repaired whenever it diverges from the JWT
48
+ * principal — even when the gateway binding already matches — because the
49
+ * /v1/messages trust path still resolves against the local mirror in this
50
+ * plan. A stale mirror must be repaired or valid guardians stay `unknown`.
34
51
  */
35
- export function healGuardianBindingDrift(incomingPrincipalId: string): boolean {
52
+ export async function healGuardianBindingDrift(
53
+ incomingPrincipalId: string,
54
+ ): Promise<boolean> {
36
55
  if (!incomingPrincipalId.startsWith("vellum-principal-")) {
37
56
  return false;
38
57
  }
39
58
 
59
+ const guardians = await getGuardianDelivery({ channelTypes: ["vellum"] });
60
+ if (!guardians) return false;
61
+ const guardian = guardianForChannel(guardians, "vellum");
62
+ if (!guardian) return false;
63
+
64
+ const currentPrincipalId = guardian.principalId;
65
+ if (!currentPrincipalId?.startsWith("vellum-principal-")) return false;
66
+
67
+ // Resolve the assistant-mirror row whose principal drives local trust.
40
68
  const guardianResult = findGuardianForChannel("vellum");
41
69
  if (!guardianResult) return false;
42
70
 
43
- const currentPrincipalId = guardianResult.contact.principalId;
44
- if (!currentPrincipalId?.startsWith("vellum-principal-")) return false;
45
- if (currentPrincipalId === incomingPrincipalId) return false;
71
+ const localPrincipalId = guardianResult.contact.principalId;
72
+ // Only repair auto-generated local principals — never overwrite a real one.
73
+ if (!localPrincipalId?.startsWith("vellum-principal-")) return false;
74
+ // No-op when the local mirror already matches the JWT principal.
75
+ if (localPrincipalId === incomingPrincipalId) return false;
46
76
 
47
77
  const updated = updateContactPrincipalAndChannel(
48
78
  guardianResult.contact.id,
@@ -53,7 +83,7 @@ export function healGuardianBindingDrift(incomingPrincipalId: string): boolean {
53
83
  if (!updated) {
54
84
  log.warn(
55
85
  {
56
- oldPrincipalId: currentPrincipalId,
86
+ oldPrincipalId: localPrincipalId,
57
87
  newPrincipalId: incomingPrincipalId,
58
88
  },
59
89
  "Skipped guardian binding drift heal — address collision on contact_channels",
@@ -63,11 +93,40 @@ export function healGuardianBindingDrift(incomingPrincipalId: string): boolean {
63
93
 
64
94
  log.info(
65
95
  {
66
- oldPrincipalId: currentPrincipalId,
96
+ oldPrincipalId: localPrincipalId,
67
97
  newPrincipalId: incomingPrincipalId,
68
98
  },
69
- "Healed vellum guardian binding drift — updated principalId to match JWT actor",
99
+ "Healed vellum guardian binding drift — updated local mirror principalId to match JWT actor",
70
100
  );
71
101
 
72
102
  return true;
73
103
  }
104
+
105
+ /**
106
+ * Re-resolve trust from the local mirror only for the narrow vellum-principal
107
+ * reset-drift case; null when it isn't drift (caller keeps the gateway verdict).
108
+ */
109
+ export async function reResolveTrustOnResetDrift(
110
+ incomingPrincipalId: string,
111
+ sourceChannel: ChannelId,
112
+ ): Promise<TrustContext | null> {
113
+ const guardians = await getGuardianDelivery({ channelTypes: ["vellum"] });
114
+ const gatewayPrincipal = guardians
115
+ ? guardianForChannel(guardians, "vellum")?.principalId
116
+ : undefined;
117
+ const isResetDrift =
118
+ incomingPrincipalId.startsWith("vellum-principal-") &&
119
+ !!gatewayPrincipal?.startsWith("vellum-principal-") &&
120
+ gatewayPrincipal !== incomingPrincipalId;
121
+ if (!isResetDrift) return null;
122
+ await healGuardianBindingDrift(incomingPrincipalId);
123
+ return withSourceChannel(
124
+ sourceChannel,
125
+ resolveTrustContext({
126
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
127
+ sourceChannel: "vellum",
128
+ conversationExternalId: "local",
129
+ actorExternalId: incomingPrincipalId,
130
+ }),
131
+ );
132
+ }
@@ -7,9 +7,14 @@
7
7
  * persisted, or returned in the outcome.
8
8
  */
9
9
 
10
+ import {
11
+ getInboundTrustVerdict,
12
+ getPhoneCallerVerdict,
13
+ } from "../calls/inbound-trust-reader.js";
10
14
  import type { ChannelId } from "../channels/types.js";
11
15
  import { findContactChannel, getContact } from "../contacts/contact-store.js";
12
16
  import { upsertContactChannel } from "../contacts/contacts-write.js";
17
+ import type { ChannelStatus } from "../contacts/types.js";
13
18
  import { ipcCallPersistent } from "../ipc/gateway-client.js";
14
19
  import { getSqlite } from "../memory/db-connection.js";
15
20
  import {
@@ -23,9 +28,27 @@ import {
23
28
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
24
29
  import { getLogger } from "../util/logger.js";
25
30
  import { hashVoiceCode } from "../util/voice-code.js";
31
+ import { verdictMemberFromVerdict } from "./trust-verdict-consumer.js";
26
32
 
27
33
  const log = getLogger("invite-redemption-service");
28
34
 
35
+ /**
36
+ * Resolve the sender's existing member status for the already_member/blocked
37
+ * gate from the gateway trust verdict. Falls back to the local channel status
38
+ * when the verdict is absent or carries no resolvable member status (e.g. an
39
+ * externalChatId-only match or a resolutionFailed verdict), so a locally
40
+ * blocked contact can't bypass the gate.
41
+ */
42
+ export async function resolveMemberGateStatus(
43
+ verdict: Awaited<ReturnType<typeof getInboundTrustVerdict>>,
44
+ localChannelStatus: ChannelStatus | null,
45
+ ): Promise<ChannelStatus | null> {
46
+ const memberStatus = verdict
47
+ ? verdictMemberFromVerdict(verdict)?.status
48
+ : null;
49
+ return memberStatus ?? localChannelStatus;
50
+ }
51
+
29
52
  // ---------------------------------------------------------------------------
30
53
  // Gateway lifecycle bridge (shared by all redemption paths)
31
54
  // ---------------------------------------------------------------------------
@@ -218,18 +241,22 @@ export async function redeemInvite(params: {
218
241
  const targetMismatch =
219
242
  existingContact && existingContact.id !== invite.contactId;
220
243
 
221
- if (
222
- existingChannel &&
223
- existingChannel.status === "active" &&
224
- !targetMismatch
225
- ) {
244
+ const gateStatus = await resolveMemberGateStatus(
245
+ await getInboundTrustVerdict({
246
+ channelType: sourceChannel as ChannelId,
247
+ actorExternalId: canonicalUserId,
248
+ }),
249
+ existingChannel?.status ?? null,
250
+ );
251
+
252
+ if (existingChannel && gateStatus === "active" && !targetMismatch) {
226
253
  return { ok: true, type: "already_member", memberId: existingChannel.id };
227
254
  }
228
255
 
229
256
  // Blocked members cannot bypass the guardian's explicit block via invite
230
257
  // links. Return the same generic failure as an invalid token to avoid
231
258
  // leaking membership status to the caller.
232
- if (existingChannel && existingChannel.status === "blocked") {
259
+ if (existingChannel && gateStatus === "blocked") {
233
260
  return { ok: false, reason: "invalid_token" };
234
261
  }
235
262
 
@@ -465,11 +492,12 @@ export async function redeemVoiceInviteCode(params: {
465
492
  // should bind the sender's identity to the target contact, not the existing one.
466
493
  const targetMismatch = voiceContact && voiceContact.id !== invite.contactId;
467
494
 
468
- if (
469
- existingVoiceChannel &&
470
- existingVoiceChannel.status === "active" &&
471
- !targetMismatch
472
- ) {
495
+ const gateStatus = await resolveMemberGateStatus(
496
+ await getPhoneCallerVerdict(canonicalCallerId),
497
+ existingVoiceChannel?.status ?? null,
498
+ );
499
+
500
+ if (existingVoiceChannel && gateStatus === "active" && !targetMismatch) {
473
501
  return {
474
502
  ok: true,
475
503
  type: "already_member",
@@ -478,7 +506,7 @@ export async function redeemVoiceInviteCode(params: {
478
506
  }
479
507
 
480
508
  // Blocked members cannot bypass the guardian's explicit block
481
- if (existingVoiceChannel && existingVoiceChannel.status === "blocked") {
509
+ if (existingVoiceChannel && gateStatus === "blocked") {
482
510
  return { ok: false, reason: "invalid_or_expired" };
483
511
  }
484
512
 
@@ -639,18 +667,22 @@ export async function redeemInviteByCode(params: {
639
667
  const targetMismatch =
640
668
  existingContact && existingContact.id !== invite.contactId;
641
669
 
642
- if (
643
- existingChannel &&
644
- existingChannel.status === "active" &&
645
- !targetMismatch
646
- ) {
670
+ const gateStatus = await resolveMemberGateStatus(
671
+ await getInboundTrustVerdict({
672
+ channelType: sourceChannel as ChannelId,
673
+ actorExternalId: canonicalUserId,
674
+ }),
675
+ existingChannel?.status ?? null,
676
+ );
677
+
678
+ if (existingChannel && gateStatus === "active" && !targetMismatch) {
647
679
  return { ok: true, type: "already_member", memberId: existingChannel.id };
648
680
  }
649
681
 
650
682
  // Blocked members cannot bypass the guardian's explicit block via invite
651
683
  // codes. Return the same generic failure as an invalid token to avoid
652
684
  // leaking membership status to the caller.
653
- if (existingChannel && existingChannel.status === "blocked") {
685
+ if (existingChannel && gateStatus === "blocked") {
654
686
  return { ok: false, reason: "invalid_token" };
655
687
  }
656
688
 
@@ -14,6 +14,11 @@
14
14
  import type { ChannelId } from "../channels/types.js";
15
15
  import { isHttpAuthDisabled } from "../config/env.js";
16
16
  import { findGuardianForChannel } from "../contacts/contact-store.js";
17
+ import {
18
+ getGuardianDelivery,
19
+ guardianForChannel,
20
+ peekCachedGuardianDelivery,
21
+ } from "../contacts/guardian-delivery-reader.js";
17
22
  import type { TrustContext } from "../daemon/trust-context.js";
18
23
  import { getLogger } from "../util/logger.js";
19
24
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
@@ -45,13 +50,48 @@ export function buildLocalAuthContext(conversationId: string): AuthContext {
45
50
  }
46
51
 
47
52
  /**
48
- * Look up the local vellum guardian's principalId from the contacts table.
53
+ * Resolve the local vellum guardian's principalId from the gateway.
49
54
  *
50
- * Returns `undefined` when no vellum guardian binding exists (e.g. fresh
51
- * install before bootstrap). Callers should treat that case as
55
+ * The gateway owns guardian binding; this reads it through the cached
56
+ * `getGuardianDelivery` reader (PR-3 TTL + single-flight) so hot paths don't
57
+ * storm the IPC. Falls back to the local contacts table for the bootstrap /
58
+ * first-run window where the gateway has no guardian yet or is unreachable
59
+ * (the reader returns `null`).
60
+ *
61
+ * Returns `undefined` when no vellum guardian binding exists in either source
62
+ * (e.g. fresh install before bootstrap). Callers should treat that case as
52
63
  * "not yet available" and either fall back or proceed without a principalId.
53
64
  */
54
- export function findLocalGuardianPrincipalId(): string | undefined {
65
+ export async function findLocalGuardianPrincipalId(): Promise<
66
+ string | undefined
67
+ > {
68
+ const list = await getGuardianDelivery({ channelTypes: ["vellum"] });
69
+ if (list) {
70
+ const principalId = guardianForChannel(list, "vellum")?.principalId;
71
+ if (principalId) return principalId;
72
+ }
73
+
74
+ return findLocalGuardianPrincipalIdFromStore();
75
+ }
76
+
77
+ /**
78
+ * Synchronous read of the vellum guardian's principalId for paths that cannot
79
+ * await {@link findLocalGuardianPrincipalId} — namely the SSE eager-subscribe
80
+ * path (`events-routes`), which registers before the stream is created.
81
+ *
82
+ * Reads the same gateway-owned binding as the async path via a sync, IO-free
83
+ * snapshot of the guardian-delivery cache (kept fresh by the async hot paths
84
+ * and event-driven invalidation), so SSE registers the SAME principal the
85
+ * send/result routes resolve. Falls back to the local store when the cache is
86
+ * cold — the same fallback the async path lands on during bootstrap.
87
+ */
88
+ export function findLocalGuardianPrincipalIdFromStore(): string | undefined {
89
+ const cached = peekCachedGuardianDelivery({ channelTypes: ["vellum"] });
90
+ if (cached) {
91
+ const principalId = guardianForChannel(cached, "vellum")?.principalId;
92
+ if (principalId) return principalId;
93
+ }
94
+
55
95
  return findGuardianForChannel("vellum")?.contact.principalId ?? undefined;
56
96
  }
57
97
 
@@ -76,12 +116,35 @@ export function findLocalGuardianPrincipalId(): string | undefined {
76
116
  * yet (e.g. fresh install before bootstrap); callers must treat this the
77
117
  * same as a missing principal.
78
118
  */
79
- export function resolveActorPrincipalIdForLocalGuardian(
119
+ export async function resolveActorPrincipalIdForLocalGuardian(
120
+ rawHeader: string | undefined,
121
+ ): Promise<string | undefined> {
122
+ if (rawHeader !== "dev-bypass" || !isHttpAuthDisabled()) return rawHeader;
123
+
124
+ const guardianPrincipalId = await findLocalGuardianPrincipalId();
125
+ if (guardianPrincipalId) return guardianPrincipalId;
126
+
127
+ log.warn(
128
+ "dev-bypass actor principal received but no vellum guardian binding found; returning undefined",
129
+ );
130
+ return undefined;
131
+ }
132
+
133
+ /**
134
+ * Synchronous variant of {@link resolveActorPrincipalIdForLocalGuardian} for
135
+ * the SSE eager-subscribe path, which registers before the response stream is
136
+ * created and cannot await. Resolves the guardian from the IO-free gateway
137
+ * cache snapshot first (same source the async path reads), falling back to the
138
+ * local store when the cache is cold — so SSE registers the SAME principal the
139
+ * send/result routes resolve and host-proxy targeting matches the same-user
140
+ * client even when the local contact row is stale.
141
+ */
142
+ export function resolveActorPrincipalIdForLocalGuardianSync(
80
143
  rawHeader: string | undefined,
81
144
  ): string | undefined {
82
145
  if (rawHeader !== "dev-bypass" || !isHttpAuthDisabled()) return rawHeader;
83
146
 
84
- const guardianPrincipalId = findLocalGuardianPrincipalId();
147
+ const guardianPrincipalId = findLocalGuardianPrincipalIdFromStore();
85
148
  if (guardianPrincipalId) return guardianPrincipalId;
86
149
 
87
150
  log.warn(
@@ -102,12 +165,12 @@ export function resolveActorPrincipalIdForLocalGuardian(
102
165
  * bootstrap), falls back to a minimal guardian context so the local
103
166
  * user is not incorrectly denied.
104
167
  */
105
- export function resolveLocalTrustContext(
168
+ export async function resolveLocalTrustContext(
106
169
  sourceChannel: ChannelId = "vellum",
107
- ): TrustContext {
170
+ ): Promise<TrustContext> {
108
171
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
109
172
 
110
- const guardianPrincipalId = findLocalGuardianPrincipalId();
173
+ const guardianPrincipalId = await findLocalGuardianPrincipalId();
111
174
  if (guardianPrincipalId) {
112
175
  const trustCtx = resolveTrustContext({
113
176
  assistantId,
@@ -139,10 +202,12 @@ export function resolveLocalTrustContext(
139
202
  * downstream code to resolve guardian context using the same
140
203
  * `authContext.actorPrincipalId` path as HTTP sessions.
141
204
  */
142
- export function resolveLocalAuthContext(conversationId: string): AuthContext {
205
+ export async function resolveLocalAuthContext(
206
+ conversationId: string,
207
+ ): Promise<AuthContext> {
143
208
  const authContext = buildLocalAuthContext(conversationId);
144
209
 
145
- const guardianPrincipalId = findLocalGuardianPrincipalId();
210
+ const guardianPrincipalId = await findLocalGuardianPrincipalId();
146
211
  if (guardianPrincipalId) {
147
212
  return { ...authContext, actorPrincipalId: guardianPrincipalId };
148
213
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Derives a local principal's {@link TrustContext} from the gateway guardian
3
+ * binding. Fails closed to unknown on a missing or null read.
4
+ */
5
+
6
+ import type { ChannelId } from "../channels/types.js";
7
+ import { getGuardianDelivery } from "../contacts/guardian-delivery-reader.js";
8
+ import type { TrustContext } from "../daemon/trust-context.js";
9
+ import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
10
+
11
+ export interface ResolveLocalPrincipalTrustInput {
12
+ actorPrincipalId: string;
13
+ sourceChannel: ChannelId;
14
+ conversationExternalId: string;
15
+ }
16
+
17
+ /** Guardian match → guardian ctx; miss or null read → unknown (fail closed). */
18
+ export async function resolveLocalPrincipalTrustContext(
19
+ input: ResolveLocalPrincipalTrustInput,
20
+ ): Promise<TrustContext> {
21
+ const unknownContext: TrustContext = {
22
+ sourceChannel: input.sourceChannel,
23
+ trustClass: "unknown",
24
+ requesterExternalUserId: input.actorPrincipalId,
25
+ requesterChatId: input.conversationExternalId,
26
+ };
27
+
28
+ // Fail closed: a null read means the gateway is unreachable — never grant
29
+ // guardian on a miss.
30
+ const guardians = await getGuardianDelivery({ channelTypes: ["vellum"] });
31
+ if (!guardians) return unknownContext;
32
+
33
+ const guardian = guardians.find(
34
+ (g) => g.principalId === input.actorPrincipalId,
35
+ );
36
+ if (!guardian) return unknownContext;
37
+
38
+ return {
39
+ sourceChannel: input.sourceChannel,
40
+ trustClass: "guardian",
41
+ guardianChatId: guardian.externalChatId ?? input.conversationExternalId,
42
+ guardianExternalUserId:
43
+ canonicalizeInboundIdentity(input.sourceChannel, guardian.address) ??
44
+ undefined,
45
+ guardianPrincipalId: guardian.principalId ?? undefined,
46
+ // Mirror toTrustContext: with no username the requester identifier is the
47
+ // canonical sender id, which for a vellum principal is actorPrincipalId.
48
+ requesterIdentifier: input.actorPrincipalId,
49
+ requesterExternalUserId: input.actorPrincipalId,
50
+ requesterChatId: input.conversationExternalId,
51
+ };
52
+ }
@@ -229,6 +229,15 @@ export function getByConversation(
229
229
  * /v1/host-browser-result, /v1/host-app-control-result, or
230
230
  * /v1/host-transfer-result after completing the operation, get a 404, and the
231
231
  * proxy timer would fire with a spurious timeout error.
232
+ *
233
+ * `question` interactions are also skipped: a new message supersedes an open
234
+ * ask_question by steering to it (see the enqueue path in
235
+ * conversation-routes.ts), which aborts the parked turn and settles the
236
+ * question via its abort signal. Clearing the entry here instead would drop it
237
+ * without settling the prompt's Promise (questions carry no `rpcResolve`
238
+ * fallback like secrets do) and would strip the steer of the entry it needs to
239
+ * fire — which can co-occur with a confirmation, since one model response can
240
+ * open both tools concurrently.
232
241
  */
233
242
  export function removeByConversation(
234
243
  conversationId: string,
@@ -244,7 +253,8 @@ export function removeByConversation(
244
253
  interaction.kind !== "host_browser" &&
245
254
  interaction.kind !== "host_app_control" &&
246
255
  interaction.kind !== "host_transfer" &&
247
- interaction.kind !== "acp_confirmation"
256
+ interaction.kind !== "acp_confirmation" &&
257
+ interaction.kind !== "question"
248
258
  ) {
249
259
  // resolve() clears the stored timer and detaches abort listeners.
250
260
  resolve(requestId, state);
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { beforeEach, describe, expect, mock, test } from "bun:test";
12
12
 
13
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
13
14
  import { IpcCallError } from "@vellumai/gateway-client/ipc-client";
14
15
 
15
16
  type IpcCall = { method: string; params?: Record<string, unknown> };
@@ -73,8 +74,16 @@ mock.module("../../channel-verification-service.js", () => ({
73
74
  getGuardianBinding: mock(() => guardianBinding),
74
75
  }));
75
76
 
76
- // Contact-store lookup that resolves the guardian's channel to downgrade.
77
- let contactChannel: { id: string; status: string } | null = null;
77
+ // Contact-store lookup that resolves the guardian's channel to downgrade. The
78
+ // channel carries the type/address/externalChatId the gateway delivery is
79
+ // matched against (see deliveryForChannel).
80
+ let contactChannel: {
81
+ id: string;
82
+ status: string;
83
+ type: string;
84
+ address: string;
85
+ externalChatId: string;
86
+ } | null = null;
78
87
  const actualContactStore = await import("../../../contacts/contact-store.js");
79
88
  mock.module("../../../contacts/contact-store.js", () => ({
80
89
  ...actualContactStore,
@@ -83,6 +92,20 @@ mock.module("../../../contacts/contact-store.js", () => ({
83
92
  ),
84
93
  }));
85
94
 
95
+ // Gateway delivery (ACL source of truth). The revoke gate relays only when the
96
+ // matching delivery is live (active/pending/unverified); already-revoked or a
97
+ // missing delivery short-circuits the relay.
98
+ let guardianDeliveries: GuardianDelivery[] | null = null;
99
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
100
+ getGuardianDelivery: mock(async (input?: { channelTypes?: string[] }) => {
101
+ if (guardianDeliveries == null) return null;
102
+ if (!input?.channelTypes) return guardianDeliveries;
103
+ return guardianDeliveries.filter((g) =>
104
+ input.channelTypes!.includes(g.channelType),
105
+ );
106
+ }),
107
+ }));
108
+
86
109
  // Guard: the contact-channel ACL write moves to the gateway relay and must
87
110
  // never run locally. The assistant-owned guardian-binding teardown
88
111
  // (revokeGuardianBinding) still runs locally — assert it is invoked.
@@ -122,7 +145,24 @@ describe("verification revoke relay", () => {
122
145
  guardianExternalUserId: "guardian-user",
123
146
  guardianDeliveryChatId: "chat-1",
124
147
  };
125
- contactChannel = { id: "ch1", status: "active" };
148
+ contactChannel = {
149
+ id: "ch1",
150
+ status: "active",
151
+ type: "telegram",
152
+ address: "guardian-user",
153
+ externalChatId: "chat-1",
154
+ };
155
+ // Gateway delivery is live by default, so the revoke relay fires.
156
+ guardianDeliveries = [
157
+ {
158
+ channelType: "telegram",
159
+ contactId: "c1",
160
+ address: "guardian-user",
161
+ externalChatId: "chat-1",
162
+ status: "active",
163
+ verifiedAt: 1700000000,
164
+ },
165
+ ];
126
166
  ipcCallPersistentMock.mockClear();
127
167
  cancelOutboundMock.mockClear();
128
168
  revokePendingSessionsMock.mockClear();
@@ -263,8 +303,19 @@ describe("verification revoke relay", () => {
263
303
  expect(result.bound).toBe(false);
264
304
  });
265
305
 
266
- test("skips the relay when the resolved channel is already revoked", async () => {
267
- contactChannel = { id: "ch1", status: "revoked" };
306
+ test("skips the relay when the gateway delivery is already revoked", async () => {
307
+ // The gateway (source of truth) already shows the channel revoked, so the
308
+ // redundant relay is skipped even though session/binding teardown runs.
309
+ guardianDeliveries = [
310
+ {
311
+ channelType: "telegram",
312
+ contactId: "c1",
313
+ address: "guardian-user",
314
+ externalChatId: "chat-1",
315
+ status: "revoked",
316
+ verifiedAt: 1700000000,
317
+ },
318
+ ];
268
319
 
269
320
  await revokeHandler({ body: { channel: "telegram" } });
270
321
 
@@ -24,7 +24,7 @@ mock.module("../../../daemon/handlers/config-channels.js", () => ({
24
24
  verifyTrustedContactCalls.push([contactChannelId, assistantId]);
25
25
  return verifyTrustedContactImpl(contactChannelId, assistantId);
26
26
  },
27
- createInboundChallenge: () => ({ success: true }),
27
+ createInboundChallenge: async () => ({ success: true }),
28
28
  getVerificationStatus: () => ({ success: true }),
29
29
  revokeVerificationForChannel: () => ({ success: true }),
30
30
  }));