@vellumai/assistant 0.4.35 → 0.4.37

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 (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -195,21 +195,24 @@ beforeEach(() => {
195
195
  // ===========================================================================
196
196
 
197
197
  describe("startOutbound", () => {
198
- test("SMS: returns missing_destination when destination is absent", () => {
199
- const result = startOutbound({ channel: "sms" });
198
+ test("SMS: returns missing_destination when destination is absent", async () => {
199
+ const result = await startOutbound({ channel: "sms" });
200
200
  expect(result.success).toBe(false);
201
201
  expect(result.error).toBe("missing_destination");
202
202
  expect(result.channel).toBe("sms");
203
203
  });
204
204
 
205
- test("SMS: returns invalid_destination for garbage phone number", () => {
206
- const result = startOutbound({ channel: "sms", destination: "notaphone" });
205
+ test("SMS: returns invalid_destination for garbage phone number", async () => {
206
+ const result = await startOutbound({
207
+ channel: "sms",
208
+ destination: "notaphone",
209
+ });
207
210
  expect(result.success).toBe(false);
208
211
  expect(result.error).toBe("invalid_destination");
209
212
  });
210
213
 
211
- test("SMS: succeeds with valid E.164 number", () => {
212
- const result = startOutbound({
214
+ test("SMS: succeeds with valid E.164 number", async () => {
215
+ const result = await startOutbound({
213
216
  channel: "sms",
214
217
  destination: "+15551234567",
215
218
  });
@@ -222,8 +225,8 @@ describe("startOutbound", () => {
222
225
  expect(result.channel).toBe("sms");
223
226
  });
224
227
 
225
- test("SMS: succeeds with loose phone format (parentheses + dashes)", () => {
226
- const result = startOutbound({
228
+ test("SMS: succeeds with loose phone format (parentheses + dashes)", async () => {
229
+ const result = await startOutbound({
227
230
  channel: "sms",
228
231
  destination: "(555) 987-6543",
229
232
  });
@@ -231,14 +234,14 @@ describe("startOutbound", () => {
231
234
  expect(result.verificationSessionId).toBeDefined();
232
235
  });
233
236
 
234
- test("Telegram: returns missing_destination when absent", () => {
235
- const result = startOutbound({ channel: "telegram" });
237
+ test("Telegram: returns missing_destination when absent", async () => {
238
+ const result = await startOutbound({ channel: "telegram" });
236
239
  expect(result.success).toBe(false);
237
240
  expect(result.error).toBe("missing_destination");
238
241
  });
239
242
 
240
- test("Telegram: succeeds with numeric chat ID", () => {
241
- const result = startOutbound({
243
+ test("Telegram: succeeds with numeric chat ID", async () => {
244
+ const result = await startOutbound({
242
245
  channel: "telegram",
243
246
  destination: "123456789",
244
247
  });
@@ -248,8 +251,8 @@ describe("startOutbound", () => {
248
251
  expect(result.sendCount).toBe(1);
249
252
  });
250
253
 
251
- test("Telegram: returns invalid_destination for negative (group) chat ID", () => {
252
- const result = startOutbound({
254
+ test("Telegram: returns invalid_destination for negative (group) chat ID", async () => {
255
+ const result = await startOutbound({
253
256
  channel: "telegram",
254
257
  destination: "-100123456",
255
258
  });
@@ -257,8 +260,8 @@ describe("startOutbound", () => {
257
260
  expect(result.error).toBe("invalid_destination");
258
261
  });
259
262
 
260
- test("Telegram: returns pending_bootstrap for handle destination", () => {
261
- const result = startOutbound({
263
+ test("Telegram: returns pending_bootstrap for handle destination", async () => {
264
+ const result = await startOutbound({
262
265
  channel: "telegram",
263
266
  destination: "@someuser",
264
267
  });
@@ -270,9 +273,9 @@ describe("startOutbound", () => {
270
273
  expect(result.secret).toBeUndefined();
271
274
  });
272
275
 
273
- test("Telegram: returns no_bot_username when bot not configured", () => {
276
+ test("Telegram: returns no_bot_username when bot not configured", async () => {
274
277
  mockBotUsername = undefined;
275
- const result = startOutbound({
278
+ const result = await startOutbound({
276
279
  channel: "telegram",
277
280
  destination: "@someuser",
278
281
  });
@@ -280,20 +283,23 @@ describe("startOutbound", () => {
280
283
  expect(result.error).toBe("no_bot_username");
281
284
  });
282
285
 
283
- test("voice: returns missing_destination when absent", () => {
284
- const result = startOutbound({ channel: "voice" });
286
+ test("voice: returns missing_destination when absent", async () => {
287
+ const result = await startOutbound({ channel: "voice" });
285
288
  expect(result.success).toBe(false);
286
289
  expect(result.error).toBe("missing_destination");
287
290
  });
288
291
 
289
- test("voice: returns invalid_destination for garbage", () => {
290
- const result = startOutbound({ channel: "voice", destination: "badphone" });
292
+ test("voice: returns invalid_destination for garbage", async () => {
293
+ const result = await startOutbound({
294
+ channel: "voice",
295
+ destination: "badphone",
296
+ });
291
297
  expect(result.success).toBe(false);
292
298
  expect(result.error).toBe("invalid_destination");
293
299
  });
294
300
 
295
- test("voice: succeeds with valid phone", () => {
296
- const result = startOutbound({
301
+ test("voice: succeeds with valid phone", async () => {
302
+ const result = await startOutbound({
297
303
  channel: "voice",
298
304
  destination: "+15559876543",
299
305
  });
@@ -303,9 +309,9 @@ describe("startOutbound", () => {
303
309
  expect(result.sendCount).toBe(1);
304
310
  });
305
311
 
306
- test("voice: passes originConversationId to startGuardianVerificationCall", () => {
312
+ test("voice: passes originConversationId to startGuardianVerificationCall", async () => {
307
313
  voiceCallInitCalls.length = 0;
308
- const result = startOutbound({
314
+ const result = await startOutbound({
309
315
  channel: "voice",
310
316
  destination: "+15559876543",
311
317
  originConversationId: "conv-origin-linkage-test",
@@ -316,9 +322,9 @@ describe("startOutbound", () => {
316
322
  expect(voiceCallInitCalls[0].phoneNumber).toBe("+15559876543");
317
323
  });
318
324
 
319
- test("voice: succeeds without originConversationId (backward compat)", () => {
325
+ test("voice: succeeds without originConversationId (backward compat)", async () => {
320
326
  voiceCallInitCalls.length = 0;
321
- const result = startOutbound({
327
+ const result = await startOutbound({
322
328
  channel: "voice",
323
329
  destination: "+15551119999",
324
330
  });
@@ -326,9 +332,9 @@ describe("startOutbound", () => {
326
332
  expect(voiceCallInitCalls.length).toBe(1);
327
333
  });
328
334
 
329
- test("unsupported channel returns unsupported_channel", () => {
335
+ test("unsupported channel returns unsupported_channel", async () => {
330
336
  // Cast to bypass type checking for test purposes
331
- const result = startOutbound({ channel: "email" as never });
337
+ const result = await startOutbound({ channel: "email" as never });
332
338
  expect(result.success).toBe(false);
333
339
  expect(result.error).toBe("unsupported_channel");
334
340
  });
@@ -345,9 +351,9 @@ describe("resendOutbound", () => {
345
351
  expect(result.error).toBe("no_active_session");
346
352
  });
347
353
 
348
- test("SMS: succeeds when an active session exists and cooldown has passed", () => {
354
+ test("SMS: succeeds when an active session exists and cooldown has passed", async () => {
349
355
  // Start a session first
350
- const startResult = startOutbound({
356
+ const startResult = await startOutbound({
351
357
  channel: "sms",
352
358
  destination: "+15551112222",
353
359
  });
@@ -369,8 +375,8 @@ describe("resendOutbound", () => {
369
375
  expect(resendResult.sendCount).toBe(2);
370
376
  });
371
377
 
372
- test("SMS: preserves originConversationId on resend", () => {
373
- const startResult = startOutbound({
378
+ test("SMS: preserves originConversationId on resend", async () => {
379
+ const startResult = await startOutbound({
374
380
  channel: "sms",
375
381
  destination: "+15551113333",
376
382
  });
@@ -395,7 +401,7 @@ describe("resendOutbound", () => {
395
401
 
396
402
  test("voice: preserves originConversationId on resend and passes it to call initiation", async () => {
397
403
  voiceCallInitCalls.length = 0;
398
- const startResult = startOutbound({
404
+ const startResult = await startOutbound({
399
405
  channel: "voice",
400
406
  destination: "+15559991111",
401
407
  });
@@ -440,8 +446,8 @@ describe("cancelOutbound", () => {
440
446
  expect(result.error).toBe("no_active_session");
441
447
  });
442
448
 
443
- test("succeeds when an active session exists", () => {
444
- const startResult = startOutbound({
449
+ test("succeeds when an active session exists", async () => {
450
+ const startResult = await startOutbound({
445
451
  channel: "sms",
446
452
  destination: "+15553334444",
447
453
  });
@@ -582,8 +588,8 @@ describe("HTTP route: handleCancelOutbound", () => {
582
588
  // ===========================================================================
583
589
 
584
590
  describe("origin conversation linkage", () => {
585
- test("startOutbound SMS echoes originConversationId in result", () => {
586
- const result = startOutbound({
591
+ test("startOutbound SMS echoes originConversationId in result", async () => {
592
+ const result = await startOutbound({
587
593
  channel: "sms",
588
594
  destination: "+15551119999",
589
595
  originConversationId: "conv-origin-sms-test",
@@ -592,8 +598,8 @@ describe("origin conversation linkage", () => {
592
598
  expect(result.originConversationId).toBe("conv-origin-sms-test");
593
599
  });
594
600
 
595
- test("startOutbound voice echoes originConversationId in result", () => {
596
- const result = startOutbound({
601
+ test("startOutbound voice echoes originConversationId in result", async () => {
602
+ const result = await startOutbound({
597
603
  channel: "voice",
598
604
  destination: "+15552229999",
599
605
  originConversationId: "conv-origin-voice-test",
@@ -602,8 +608,8 @@ describe("origin conversation linkage", () => {
602
608
  expect(result.originConversationId).toBe("conv-origin-voice-test");
603
609
  });
604
610
 
605
- test("startOutbound Telegram (chat ID) echoes originConversationId in result", () => {
606
- const result = startOutbound({
611
+ test("startOutbound Telegram (chat ID) echoes originConversationId in result", async () => {
612
+ const result = await startOutbound({
607
613
  channel: "telegram",
608
614
  destination: "999888777",
609
615
  originConversationId: "conv-origin-tg-test",
@@ -612,8 +618,8 @@ describe("origin conversation linkage", () => {
612
618
  expect(result.originConversationId).toBe("conv-origin-tg-test");
613
619
  });
614
620
 
615
- test("startOutbound without originConversationId returns undefined for field", () => {
616
- const result = startOutbound({
621
+ test("startOutbound without originConversationId returns undefined for field", async () => {
622
+ const result = await startOutbound({
617
623
  channel: "sms",
618
624
  destination: "+15553338888",
619
625
  });
@@ -635,7 +641,7 @@ describe("origin conversation linkage", () => {
635
641
  });
636
642
 
637
643
  test("voice call initiation receives originConversationId", async () => {
638
- const result = startOutbound({
644
+ const result = await startOutbound({
639
645
  channel: "voice",
640
646
  destination: "+15554443333",
641
647
  originConversationId: "conv-origin-voice-init",
@@ -656,7 +662,7 @@ describe("origin conversation linkage", () => {
656
662
  voiceCallInitCalls.length = 0;
657
663
 
658
664
  // Start a voice session (no origin initially)
659
- const startResult = startOutbound({
665
+ const startResult = await startOutbound({
660
666
  channel: "voice",
661
667
  destination: "+15552228888",
662
668
  });
@@ -40,10 +40,33 @@ mock.module("../config/loader.js", () => {
40
40
  applyNestedDefaults: (c: unknown) => c,
41
41
  getNestedValue: () => undefined,
42
42
  setNestedValue: () => {},
43
+ syncConfigToLockfile: () => {},
43
44
  API_KEY_PROVIDERS: [],
44
45
  };
45
46
  });
46
47
 
48
+ const noopLogger = {
49
+ info: () => {},
50
+ warn: () => {},
51
+ error: () => {},
52
+ debug: () => {},
53
+ trace: () => {},
54
+ fatal: () => {},
55
+ child: () => noopLogger,
56
+ };
57
+
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const realLogger = require("../util/logger.js");
60
+ mock.module("../util/logger.js", () => ({
61
+ ...realLogger,
62
+ getLogger: () => noopLogger,
63
+ getCliLogger: () => noopLogger,
64
+ isDebug: () => false,
65
+ truncateForLog: (v: string) => v,
66
+ initLogger: () => {},
67
+ pruneOldLogFiles: () => 0,
68
+ }));
69
+
47
70
  const { handleUserMessage } = await import("../daemon/handlers/sessions.js");
48
71
 
49
72
  describe("handleUserMessage secret redirect continuation", () => {
@@ -6,41 +6,50 @@ import { beforeEach, describe, expect, test } from "bun:test";
6
6
  import { mock } from "bun:test";
7
7
 
8
8
  const testDir = mkdtempSync(join(tmpdir(), "trust-rule-metadata-test-"));
9
+ const resolveTestDir = () => process.env.BASE_DATA_DIR ?? testDir;
9
10
 
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const realPlatform = require("../util/platform.js");
10
13
  mock.module("../util/platform.js", () => ({
11
- getRootDir: () => testDir,
12
- getDataDir: () => join(testDir, "data"),
13
- getWorkspaceSkillsDir: () => join(testDir, "skills"),
14
+ ...realPlatform,
15
+ getRootDir: () => resolveTestDir(),
16
+ getDataDir: () => join(resolveTestDir(), "data"),
17
+ getWorkspaceSkillsDir: () => join(resolveTestDir(), "skills"),
14
18
  isMacOS: () => process.platform === "darwin",
15
19
  isLinux: () => process.platform === "linux",
16
20
  isWindows: () => process.platform === "win32",
17
- getSocketPath: () => join(testDir, "test.sock"),
18
- getPidPath: () => join(testDir, "test.pid"),
19
- getDbPath: () => join(testDir, "test.db"),
20
- getLogPath: () => join(testDir, "test.log"),
21
+ getSocketPath: () => join(resolveTestDir(), "test.sock"),
22
+ getPidPath: () => join(resolveTestDir(), "test.pid"),
23
+ getDbPath: () => join(resolveTestDir(), "test.db"),
24
+ getLogPath: () => join(resolveTestDir(), "test.log"),
21
25
  ensureDataDir: () => {},
22
- getIpcBlobDir: () => join(testDir, "ipc-blobs"),
26
+ getIpcBlobDir: () => join(resolveTestDir(), "ipc-blobs"),
23
27
  }));
24
28
 
29
+ const noopLogger = {
30
+ info: () => {},
31
+ warn: () => {},
32
+ error: () => {},
33
+ debug: () => {},
34
+ trace: () => {},
35
+ fatal: () => {},
36
+ child: () => noopLogger,
37
+ };
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const realLogger = require("../util/logger.js");
25
41
  mock.module("../util/logger.js", () => ({
26
- getLogger: () => ({
27
- info: () => {},
28
- warn: () => {},
29
- error: () => {},
30
- debug: () => {},
31
- trace: () => {},
32
- fatal: () => {},
33
- child: () => ({
34
- info: () => {},
35
- warn: () => {},
36
- error: () => {},
37
- debug: () => {},
38
- }),
39
- }),
42
+ ...realLogger,
43
+ getLogger: () => noopLogger,
44
+ getCliLogger: () => noopLogger,
45
+ isDebug: () => false,
46
+ truncateForLog: (value: string) => value,
47
+ initLogger: () => {},
48
+ pruneOldLogFiles: () => 0,
40
49
  }));
41
50
 
42
51
  const testConfig: Record<string, any> = {
43
- permissions: { mode: "legacy" as "legacy" | "strict" | "workspace" },
52
+ permissions: { mode: "workspace" as "strict" | "workspace" },
44
53
  skills: { load: { extraDirs: [] as string[] } },
45
54
  sandbox: { enabled: true },
46
55
  };
@@ -22,7 +22,10 @@ mock.module("../config/loader.js", () => ({
22
22
  invalidateConfigCache: () => {},
23
23
  }));
24
24
 
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const realPlatform = require("../util/platform.js");
25
27
  mock.module("../util/platform.js", () => ({
28
+ ...realPlatform,
26
29
  getRootDir: () => testDir,
27
30
  getDataDir: () => testDir,
28
31
  getIpcBlobDir: () => join(testDir, "ipc-blobs"),
@@ -36,7 +39,10 @@ mock.module("../util/platform.js", () => ({
36
39
  ensureDataDir: () => {},
37
40
  }));
38
41
 
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const realLogger = require("../util/logger.js");
39
44
  mock.module("../util/logger.js", () => ({
45
+ ...realLogger,
40
46
  getLogger: () => ({
41
47
  info: () => {},
42
48
  warn: () => {},
@@ -70,9 +76,9 @@ mock.module("../security/secure-keys.js", () => ({
70
76
  deleteSecureKey: (account: string) => {
71
77
  if (account in secureKeyStore) {
72
78
  delete secureKeyStore[account];
73
- return true;
79
+ return "deleted";
74
80
  }
75
- return false;
81
+ return "not-found";
76
82
  },
77
83
  listSecureKeys: () => Object.keys(secureKeyStore),
78
84
  getBackendType: () => "encrypted",
@@ -71,9 +71,9 @@ mock.module("../security/secure-keys.js", () => ({
71
71
  deleteSecureKey: (account: string) => {
72
72
  if (account in secureKeyStore) {
73
73
  delete secureKeyStore[account];
74
- return true;
74
+ return "deleted";
75
75
  }
76
- return false;
76
+ return "not-found";
77
77
  },
78
78
  listSecureKeys: () => Object.keys(secureKeyStore),
79
79
  getBackendType: () => "encrypted",
@@ -1,5 +1,5 @@
1
1
  import * as net from "node:net";
2
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
3
3
 
4
4
  mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
5
5
 
@@ -47,13 +47,102 @@ mock.module("../runtime/guardian-reply-router.js", () => ({
47
47
  routeGuardianReply: routeGuardianReplyMock,
48
48
  }));
49
49
 
50
+ // Bun's module mocks are global within the worker, so keep this mock
51
+ // transparent when this file is not actively exercising it.
52
+ const realCanonicalGuardianStore =
53
+ await import("../memory/canonical-guardian-store.js");
54
+ (
55
+ globalThis as Record<string, unknown>
56
+ ).__approvalConsumptionUseMockCanonicalStore = false;
57
+
50
58
  mock.module("../memory/canonical-guardian-store.js", () => ({
51
- createCanonicalGuardianRequest: createCanonicalGuardianRequestMock,
52
- generateCanonicalRequestCode: generateCanonicalRequestCodeMock,
53
- listPendingCanonicalGuardianRequestsByDestinationConversation:
54
- listPendingByDestinationMock,
55
- listCanonicalGuardianRequests: listCanonicalMock,
56
- resolveCanonicalGuardianRequest: resolveCanonicalGuardianRequestMock,
59
+ createCanonicalGuardianRequest: (
60
+ ...args: Parameters<
61
+ typeof realCanonicalGuardianStore.createCanonicalGuardianRequest
62
+ >
63
+ ) =>
64
+ (globalThis as Record<string, unknown>)
65
+ .__approvalConsumptionUseMockCanonicalStore
66
+ ? (
67
+ createCanonicalGuardianRequestMock as unknown as (
68
+ ...mockArgs: Parameters<
69
+ typeof realCanonicalGuardianStore.createCanonicalGuardianRequest
70
+ >
71
+ ) => ReturnType<
72
+ typeof realCanonicalGuardianStore.createCanonicalGuardianRequest
73
+ >
74
+ )(...args)
75
+ : realCanonicalGuardianStore.createCanonicalGuardianRequest(...args),
76
+ generateCanonicalRequestCode: (
77
+ ...args: Parameters<
78
+ typeof realCanonicalGuardianStore.generateCanonicalRequestCode
79
+ >
80
+ ) =>
81
+ (globalThis as Record<string, unknown>)
82
+ .__approvalConsumptionUseMockCanonicalStore
83
+ ? (
84
+ generateCanonicalRequestCodeMock as unknown as (
85
+ ...mockArgs: Parameters<
86
+ typeof realCanonicalGuardianStore.generateCanonicalRequestCode
87
+ >
88
+ ) => ReturnType<
89
+ typeof realCanonicalGuardianStore.generateCanonicalRequestCode
90
+ >
91
+ )(...args)
92
+ : realCanonicalGuardianStore.generateCanonicalRequestCode(...args),
93
+ listPendingCanonicalGuardianRequestsByDestinationConversation: (
94
+ ...args: Parameters<
95
+ typeof realCanonicalGuardianStore.listPendingCanonicalGuardianRequestsByDestinationConversation
96
+ >
97
+ ) =>
98
+ (globalThis as Record<string, unknown>)
99
+ .__approvalConsumptionUseMockCanonicalStore
100
+ ? (
101
+ listPendingByDestinationMock as unknown as (
102
+ ...mockArgs: Parameters<
103
+ typeof realCanonicalGuardianStore.listPendingCanonicalGuardianRequestsByDestinationConversation
104
+ >
105
+ ) => ReturnType<
106
+ typeof realCanonicalGuardianStore.listPendingCanonicalGuardianRequestsByDestinationConversation
107
+ >
108
+ )(...args)
109
+ : realCanonicalGuardianStore.listPendingCanonicalGuardianRequestsByDestinationConversation(
110
+ ...args,
111
+ ),
112
+ listCanonicalGuardianRequests: (
113
+ ...args: Parameters<
114
+ typeof realCanonicalGuardianStore.listCanonicalGuardianRequests
115
+ >
116
+ ) =>
117
+ (globalThis as Record<string, unknown>)
118
+ .__approvalConsumptionUseMockCanonicalStore
119
+ ? (
120
+ listCanonicalMock as unknown as (
121
+ ...mockArgs: Parameters<
122
+ typeof realCanonicalGuardianStore.listCanonicalGuardianRequests
123
+ >
124
+ ) => ReturnType<
125
+ typeof realCanonicalGuardianStore.listCanonicalGuardianRequests
126
+ >
127
+ )(...args)
128
+ : realCanonicalGuardianStore.listCanonicalGuardianRequests(...args),
129
+ resolveCanonicalGuardianRequest: (
130
+ ...args: Parameters<
131
+ typeof realCanonicalGuardianStore.resolveCanonicalGuardianRequest
132
+ >
133
+ ) =>
134
+ (globalThis as Record<string, unknown>)
135
+ .__approvalConsumptionUseMockCanonicalStore
136
+ ? (
137
+ resolveCanonicalGuardianRequestMock as unknown as (
138
+ ...mockArgs: Parameters<
139
+ typeof realCanonicalGuardianStore.resolveCanonicalGuardianRequest
140
+ >
141
+ ) => ReturnType<
142
+ typeof realCanonicalGuardianStore.resolveCanonicalGuardianRequest
143
+ >
144
+ )(...args)
145
+ : realCanonicalGuardianStore.resolveCanonicalGuardianRequest(...args),
57
146
  }));
58
147
 
59
148
  mock.module("../runtime/pending-interactions.js", () => ({
@@ -98,7 +187,10 @@ mock.module("../runtime/local-actor-identity.js", () => ({
98
187
  }),
99
188
  }));
100
189
 
190
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
191
+ const realLogger = require("../util/logger.js");
101
192
  mock.module("../util/logger.js", () => ({
193
+ ...realLogger,
102
194
  getLogger: () => ({
103
195
  info: () => {},
104
196
  warn: () => {},
@@ -214,6 +306,9 @@ function makeSession(overrides: Partial<TestSession> = {}): TestSession {
214
306
 
215
307
  describe("handleUserMessage pending-confirmation reply interception", () => {
216
308
  beforeEach(() => {
309
+ (
310
+ globalThis as Record<string, unknown>
311
+ ).__approvalConsumptionUseMockCanonicalStore = true;
217
312
  routeGuardianReplyMock.mockClear();
218
313
  createCanonicalGuardianRequestMock.mockClear();
219
314
  generateCanonicalRequestCodeMock.mockClear();
@@ -227,6 +322,12 @@ describe("handleUserMessage pending-confirmation reply interception", () => {
227
322
  getConfigMock.mockClear();
228
323
  });
229
324
 
325
+ afterAll(() => {
326
+ (
327
+ globalThis as Record<string, unknown>
328
+ ).__approvalConsumptionUseMockCanonicalStore = false;
329
+ });
330
+
230
331
  test("consumes decision replies before auto-deny", async () => {
231
332
  listPendingByDestinationMock.mockReturnValue([
232
333
  { id: "req-1", kind: "tool_approval" },
@@ -24,7 +24,10 @@ mock.module("../config/loader.js", () => ({
24
24
  invalidateConfigCache: () => {},
25
25
  }));
26
26
 
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const realPlatform = require("../util/platform.js");
27
29
  mock.module("../util/platform.js", () => ({
30
+ ...realPlatform,
28
31
  getRootDir: () => testDir,
29
32
  getDataDir: () => testDir,
30
33
  getIpcBlobDir: () => join(testDir, "ipc-blobs"),
@@ -38,7 +41,10 @@ mock.module("../util/platform.js", () => ({
38
41
  ensureDataDir: () => {},
39
42
  }));
40
43
 
44
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
45
+ const realLogger = require("../util/logger.js");
41
46
  mock.module("../util/logger.js", () => ({
47
+ ...realLogger,
42
48
  getLogger: () => ({
43
49
  info: () => {},
44
50
  warn: () => {},
@@ -6,7 +6,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
6
6
  // ── Mock platform to isolate tests from the real workspace ────────────
7
7
  const TEST_DIR = join(tmpdir(), `vellum-routing-test-${crypto.randomUUID()}`);
8
8
 
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
+ const realPlatform = require("../util/platform.js");
9
11
  mock.module("../util/platform.js", () => ({
12
+ ...realPlatform,
10
13
  getRootDir: () => TEST_DIR,
11
14
  getDataDir: () => TEST_DIR,
12
15
  getWorkspaceDir: () => TEST_DIR,
@@ -30,19 +33,27 @@ mock.module("../util/platform.js", () => ({
30
33
  isWindows: () => process.platform === "win32",
31
34
  getPlatformName: () => process.platform,
32
35
  getClipboardCommand: () => null,
36
+ readSessionToken: () => null,
33
37
  removeSocketFile: () => {},
34
38
  migratePath: () => {},
35
39
  migrateToWorkspaceLayout: () => {},
36
40
  migrateToDataLayout: () => {},
37
41
  }));
38
42
 
43
+ const noopLogger = new Proxy({} as Record<string, unknown>, {
44
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
45
+ });
46
+
47
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
48
+ const realLogger = require("../util/logger.js");
39
49
  mock.module("../util/logger.js", () => ({
40
- getLogger: () =>
41
- new Proxy({} as Record<string, unknown>, {
42
- get: () => () => {},
43
- }),
50
+ ...realLogger,
51
+ getLogger: () => noopLogger,
52
+ getCliLogger: () => noopLogger,
44
53
  isDebug: () => false,
45
54
  truncateForLog: (v: string) => v,
55
+ initLogger: () => {},
56
+ pruneOldLogFiles: () => 0,
46
57
  }));
47
58
 
48
59
  mock.module("../config/loader.js", () => ({
@@ -54,6 +65,14 @@ mock.module("../config/loader.js", () => ({
54
65
  "feature_flags.guardian-verify-setup.enabled": true,
55
66
  },
56
67
  }),
68
+ loadConfig: () => ({}),
69
+ loadRawConfig: () => ({}),
70
+ saveConfig: () => {},
71
+ saveRawConfig: () => {},
72
+ invalidateConfigCache: () => {},
73
+ getNestedValue: () => undefined,
74
+ setNestedValue: () => {},
75
+ syncConfigToLockfile: () => {},
57
76
  }));
58
77
 
59
78
  // ── Import after mocks ───────────────────────────────────────────────
@@ -24,6 +24,18 @@ mock.module("../util/logger.js", () => ({
24
24
  }),
25
25
  }));
26
26
 
27
+ // Prevent ensureTelegramBotUsernameResolved() from reading real credentials
28
+ // and calling the Telegram API, which would populate credential metadata
29
+ // with the real bot username and shadow the env var override in tests.
30
+ mock.module("../security/secure-keys.js", () => ({
31
+ getSecureKey: () => undefined,
32
+ setSecureKey: () => {},
33
+ deleteSecureKey: () => {},
34
+ getSecureKeyAsync: async () => undefined,
35
+ setSecureKeyAsync: async () => {},
36
+ deleteSecureKeyAsync: async () => {},
37
+ }));
38
+
27
39
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
28
40
  import {
29
41
  handleCreateInvite,