@vellumai/assistant 0.4.9 → 0.4.11

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 (116) hide show
  1. package/ARCHITECTURE.md +24 -0
  2. package/Dockerfile +1 -1
  3. package/README.md +16 -9
  4. package/package.json +1 -1
  5. package/src/__tests__/account-registry.test.ts +1 -0
  6. package/src/__tests__/actor-token-service.test.ts +1 -0
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  8. package/src/__tests__/asset-materialize-tool.test.ts +7 -0
  9. package/src/__tests__/asset-search-tool.test.ts +7 -0
  10. package/src/__tests__/browser-fill-credential.test.ts +1 -0
  11. package/src/__tests__/call-start-guardian-guard.test.ts +1 -0
  12. package/src/__tests__/channel-approval-routes.test.ts +29 -0
  13. package/src/__tests__/channel-guardian.test.ts +2143 -1546
  14. package/src/__tests__/channel-retry-sweep.test.ts +169 -14
  15. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -0
  16. package/src/__tests__/computer-use-tools.test.ts +1 -0
  17. package/src/__tests__/contacts-tools.test.ts +1 -0
  18. package/src/__tests__/conversation-attention-telegram.test.ts +1 -0
  19. package/src/__tests__/credential-policy-validate.test.ts +97 -0
  20. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  21. package/src/__tests__/credential-vault-unit.test.ts +1 -0
  22. package/src/__tests__/credential-vault.test.ts +1 -0
  23. package/src/__tests__/delete-managed-skill-tool.test.ts +1 -0
  24. package/src/__tests__/file-edit-tool.test.ts +1 -0
  25. package/src/__tests__/file-read-tool.test.ts +1 -0
  26. package/src/__tests__/file-write-tool.test.ts +1 -0
  27. package/src/__tests__/followup-tools.test.ts +1 -0
  28. package/src/__tests__/gateway-only-guard.test.ts +1 -1
  29. package/src/__tests__/guardian-control-plane-policy.test.ts +5 -4
  30. package/src/__tests__/guardian-grant-minting.test.ts +3 -0
  31. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +4 -3
  32. package/src/__tests__/guardian-routing-state.test.ts +8 -0
  33. package/src/__tests__/headless-browser-interactions.test.ts +1 -0
  34. package/src/__tests__/headless-browser-navigate.test.ts +1 -0
  35. package/src/__tests__/headless-browser-read-tools.test.ts +1 -0
  36. package/src/__tests__/headless-browser-snapshot.test.ts +1 -0
  37. package/src/__tests__/host-file-edit-tool.test.ts +1 -0
  38. package/src/__tests__/host-file-read-tool.test.ts +1 -0
  39. package/src/__tests__/host-file-write-tool.test.ts +1 -0
  40. package/src/__tests__/host-shell-tool.test.ts +1 -0
  41. package/src/__tests__/lifecycle-docs-guard.test.ts +207 -0
  42. package/src/__tests__/managed-skill-lifecycle.test.ts +1 -0
  43. package/src/__tests__/media-reuse-story.e2e.test.ts +8 -0
  44. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  45. package/src/__tests__/playbook-execution.test.ts +1 -0
  46. package/src/__tests__/playbook-tools.test.ts +1 -0
  47. package/src/__tests__/relay-server.test.ts +4 -0
  48. package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -0
  49. package/src/__tests__/schedule-tools.test.ts +1 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -0
  51. package/src/__tests__/secret-scanner-executor.test.ts +2 -0
  52. package/src/__tests__/send-notification-tool.test.ts +2 -0
  53. package/src/__tests__/shell-credential-ref.test.ts +1 -0
  54. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -0
  55. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  56. package/src/__tests__/skill-load-tool.test.ts +1 -0
  57. package/src/__tests__/skill-script-runner-host.test.ts +1 -0
  58. package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -0
  59. package/src/__tests__/skill-script-runner.test.ts +1 -0
  60. package/src/__tests__/skill-tool-factory.test.ts +1 -0
  61. package/src/__tests__/subagent-tools.test.ts +1 -1
  62. package/src/__tests__/swarm-recursion.test.ts +1 -0
  63. package/src/__tests__/swarm-session-integration.test.ts +1 -0
  64. package/src/__tests__/swarm-tool.test.ts +1 -0
  65. package/src/__tests__/task-management-tools.test.ts +1 -0
  66. package/src/__tests__/task-tools.test.ts +1 -0
  67. package/src/__tests__/terminal-tools.test.ts +1 -0
  68. package/src/__tests__/tool-approval-handler.test.ts +2 -2
  69. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  70. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  71. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  72. package/src/__tests__/tool-executor-shell-integration.test.ts +1 -0
  73. package/src/__tests__/tool-executor.test.ts +1 -0
  74. package/src/__tests__/trust-context-guards.test.ts +218 -0
  75. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  76. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +6 -0
  77. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -0
  78. package/src/__tests__/trusted-contact-verification.test.ts +1 -0
  79. package/src/__tests__/view-image-tool.test.ts +1 -0
  80. package/src/calls/guardian-dispatch.ts +4 -4
  81. package/src/cli/mcp.ts +183 -3
  82. package/src/config/bundled-skills/agentmail/SKILL.md +4 -4
  83. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +1 -0
  84. package/src/config/bundled-skills/phone-calls/SKILL.md +17 -119
  85. package/src/config/system-prompt.ts +4 -2
  86. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  87. package/src/daemon/computer-use-session.ts +1 -0
  88. package/src/daemon/session-agent-loop.ts +1 -1
  89. package/src/daemon/session-memory.ts +2 -2
  90. package/src/daemon/session-runtime-assembly.ts +2 -2
  91. package/src/daemon/session-tool-setup.ts +1 -1
  92. package/src/mcp/client.ts +55 -6
  93. package/src/mcp/manager.ts +9 -0
  94. package/src/mcp/mcp-oauth-provider.ts +347 -0
  95. package/src/memory/channel-delivery-store.ts +1 -0
  96. package/src/memory/db-init.ts +4 -0
  97. package/src/memory/delivery-status.ts +43 -0
  98. package/src/memory/guardian-bindings.ts +3 -3
  99. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +108 -0
  100. package/src/memory/migrations/index.ts +1 -0
  101. package/src/memory/migrations/registry.ts +6 -0
  102. package/src/memory/schema.ts +1 -1
  103. package/src/runtime/actor-trust-resolver.ts +13 -4
  104. package/src/runtime/channel-retry-sweep.ts +31 -14
  105. package/src/runtime/guardian-context-resolver.ts +25 -64
  106. package/src/runtime/guardian-outbound-actions.ts +399 -108
  107. package/src/runtime/guardian-vellum-migration.ts +1 -23
  108. package/src/runtime/guardian-verification-templates.ts +66 -30
  109. package/src/runtime/local-actor-identity.ts +4 -6
  110. package/src/runtime/middleware/actor-token.ts +2 -8
  111. package/src/runtime/routes/channel-route-shared.ts +0 -1
  112. package/src/runtime/routes/inbound-message-handler.ts +3 -4
  113. package/src/runtime/tool-grant-request-helper.ts +1 -1
  114. package/src/tools/credentials/policy-validate.ts +22 -0
  115. package/src/tools/guardian-control-plane-policy.ts +2 -2
  116. package/src/tools/types.ts +1 -1
@@ -7,17 +7,17 @@
7
7
  * IPC handler (config-channels.ts) and the HTTP route layer (integration-routes.ts).
8
8
  */
9
9
 
10
- import { createHash,randomBytes } from 'node:crypto';
11
-
12
- import { startGuardianVerificationCall } from '../calls/call-domain.js';
13
- import type { ChannelId } from '../channels/types.js';
14
- import { getGatewayInternalBaseUrl } from '../config/env.js';
15
- import { sendMessage as sendSms } from '../messaging/providers/sms/client.js';
16
- import { getCredentialMetadata } from '../tools/credentials/metadata-store.js';
17
- import { getLogger } from '../util/logger.js';
18
- import { normalizePhoneNumber } from '../util/phone.js';
19
- import { readHttpToken } from '../util/platform.js';
20
- import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
10
+ import { createHash, randomBytes } from "node:crypto";
11
+
12
+ import { startGuardianVerificationCall } from "../calls/call-domain.js";
13
+ import type { ChannelId } from "../channels/types.js";
14
+ import { getGatewayInternalBaseUrl } from "../config/env.js";
15
+ import { sendMessage as sendSms } from "../messaging/providers/sms/client.js";
16
+ import { getCredentialMetadata } from "../tools/credentials/metadata-store.js";
17
+ import { getLogger } from "../util/logger.js";
18
+ import { normalizePhoneNumber } from "../util/phone.js";
19
+ import { readHttpToken } from "../util/platform.js";
20
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
21
21
  import {
22
22
  countRecentSendsToDestination,
23
23
  createOutboundSession,
@@ -25,14 +25,15 @@ import {
25
25
  getGuardianBinding,
26
26
  updateSessionDelivery,
27
27
  updateSessionStatus,
28
- } from './channel-guardian-service.js';
28
+ } from "./channel-guardian-service.js";
29
29
  import {
30
+ composeVerificationSlack,
30
31
  composeVerificationSms,
31
32
  composeVerificationTelegram,
32
33
  GUARDIAN_VERIFY_TEMPLATE_KEYS,
33
- } from './guardian-verification-templates.js';
34
+ } from "./guardian-verification-templates.js";
34
35
 
35
- const log = getLogger('guardian-outbound-actions');
36
+ const log = getLogger("guardian-outbound-actions");
36
37
 
37
38
  // ---------------------------------------------------------------------------
38
39
  // Rate limit constants for outbound verification
@@ -73,7 +74,7 @@ function isTelegramChatId(destination: string): boolean {
73
74
  */
74
75
  export function normalizeTelegramDestination(destination: string): string {
75
76
  if (isTelegramChatId(destination)) return destination;
76
- return destination.replace(/^@/, '').toLowerCase();
77
+ return destination.replace(/^@/, "").toLowerCase();
77
78
  }
78
79
 
79
80
  /**
@@ -81,8 +82,12 @@ export function normalizeTelegramDestination(destination: string): string {
81
82
  * Falls back to process.env.TELEGRAM_BOT_USERNAME.
82
83
  */
83
84
  function getTelegramBotUsername(): string | undefined {
84
- const meta = getCredentialMetadata('telegram', 'bot_token');
85
- if (meta?.accountInfo && typeof meta.accountInfo === 'string' && meta.accountInfo.trim().length > 0) {
85
+ const meta = getCredentialMetadata("telegram", "bot_token");
86
+ if (
87
+ meta?.accountInfo &&
88
+ typeof meta.accountInfo === "string" &&
89
+ meta.accountInfo.trim().length > 0
90
+ ) {
86
91
  return meta.accountInfo.trim();
87
92
  }
88
93
  return process.env.TELEGRAM_BOT_USERNAME || undefined;
@@ -150,13 +155,15 @@ function deliverVerificationSms(
150
155
  const gatewayUrl = getGatewayInternalBaseUrl();
151
156
  const bearerToken = readHttpToken();
152
157
  if (!bearerToken) {
153
- log.error('Cannot deliver verification SMS: no runtime HTTP token available');
158
+ log.error(
159
+ "Cannot deliver verification SMS: no runtime HTTP token available",
160
+ );
154
161
  return;
155
162
  }
156
163
  await sendSms(gatewayUrl, bearerToken, to, text, assistantId);
157
- log.info({ to, assistantId }, 'Verification SMS delivered');
164
+ log.info({ to, assistantId }, "Verification SMS delivered");
158
165
  } catch (err) {
159
- log.error({ err, to, assistantId }, 'Failed to deliver verification SMS');
166
+ log.error({ err, to, assistantId }, "Failed to deliver verification SMS");
160
167
  }
161
168
  })();
162
169
  }
@@ -179,26 +186,37 @@ function deliverVerificationTelegram(
179
186
  const gatewayUrl = getGatewayInternalBaseUrl();
180
187
  const bearerToken = readHttpToken();
181
188
  if (!bearerToken) {
182
- log.error('Cannot deliver verification Telegram message: no runtime HTTP token available');
189
+ log.error(
190
+ "Cannot deliver verification Telegram message: no runtime HTTP token available",
191
+ );
183
192
  return;
184
193
  }
185
194
  const url = `${gatewayUrl}/deliver/telegram`;
186
195
  const resp = await fetch(url, {
187
- method: 'POST',
196
+ method: "POST",
188
197
  headers: {
189
- 'Content-Type': 'application/json',
198
+ "Content-Type": "application/json",
190
199
  Authorization: `Bearer ${bearerToken}`,
191
200
  },
192
201
  body: JSON.stringify({ chatId, text, assistantId }),
193
202
  });
194
203
  if (!resp.ok) {
195
- const body = await resp.text().catch(() => '<unreadable>');
196
- log.error({ chatId, assistantId, status: resp.status, body }, 'Gateway /deliver/telegram failed for verification');
204
+ const body = await resp.text().catch(() => "<unreadable>");
205
+ log.error(
206
+ { chatId, assistantId, status: resp.status, body },
207
+ "Gateway /deliver/telegram failed for verification",
208
+ );
197
209
  } else {
198
- log.info({ chatId, assistantId }, 'Verification Telegram message delivered');
210
+ log.info(
211
+ { chatId, assistantId },
212
+ "Verification Telegram message delivered",
213
+ );
199
214
  }
200
215
  } catch (err) {
201
- log.error({ err, chatId, assistantId }, 'Failed to deliver verification Telegram message');
216
+ log.error(
217
+ { err, chatId, assistantId },
218
+ "Failed to deliver verification Telegram message",
219
+ );
202
220
  }
203
221
  })();
204
222
  }
@@ -226,12 +244,25 @@ function initiateGuardianVoiceCall(
226
244
  originConversationId,
227
245
  });
228
246
  if (result.ok) {
229
- log.info({ phoneNumber, guardianVerificationSessionId, callSid: result.callSid }, 'Guardian verification call initiated');
247
+ log.info(
248
+ {
249
+ phoneNumber,
250
+ guardianVerificationSessionId,
251
+ callSid: result.callSid,
252
+ },
253
+ "Guardian verification call initiated",
254
+ );
230
255
  } else {
231
- log.error({ phoneNumber, guardianVerificationSessionId, error: result.error }, 'Failed to initiate guardian verification call');
256
+ log.error(
257
+ { phoneNumber, guardianVerificationSessionId, error: result.error },
258
+ "Failed to initiate guardian verification call",
259
+ );
232
260
  }
233
261
  } catch (err) {
234
- log.error({ err, phoneNumber, guardianVerificationSessionId }, 'Failed to initiate guardian verification call');
262
+ log.error(
263
+ { err, phoneNumber, guardianVerificationSessionId },
264
+ "Failed to initiate guardian verification call",
265
+ );
235
266
  }
236
267
  })();
237
268
  }
@@ -240,23 +271,51 @@ function initiateGuardianVoiceCall(
240
271
  // Start outbound
241
272
  // ---------------------------------------------------------------------------
242
273
 
243
- export function startOutbound(params: StartOutboundParams): OutboundActionResult {
274
+ export function startOutbound(
275
+ params: StartOutboundParams,
276
+ ): OutboundActionResult {
244
277
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
245
278
  const channel = params.channel;
246
279
  const originConversationId = params.originConversationId;
247
280
 
248
- if (channel === 'sms') {
249
- return startOutboundSms(params.destination, assistantId, channel, params.rebind, originConversationId);
250
- } else if (channel === 'telegram') {
251
- return startOutboundTelegram(params.destination, assistantId, channel, params.rebind, originConversationId);
252
- } else if (channel === 'voice') {
253
- return startOutboundVoice(params.destination, assistantId, channel, params.rebind, originConversationId);
281
+ if (channel === "sms") {
282
+ return startOutboundSms(
283
+ params.destination,
284
+ assistantId,
285
+ channel,
286
+ params.rebind,
287
+ originConversationId,
288
+ );
289
+ } else if (channel === "telegram") {
290
+ return startOutboundTelegram(
291
+ params.destination,
292
+ assistantId,
293
+ channel,
294
+ params.rebind,
295
+ originConversationId,
296
+ );
297
+ } else if (channel === "voice") {
298
+ return startOutboundVoice(
299
+ params.destination,
300
+ assistantId,
301
+ channel,
302
+ params.rebind,
303
+ originConversationId,
304
+ );
305
+ } else if (channel === "slack") {
306
+ return startOutboundSlack(
307
+ params.destination,
308
+ assistantId,
309
+ channel,
310
+ params.rebind,
311
+ originConversationId,
312
+ );
254
313
  }
255
314
 
256
315
  return {
257
316
  success: false,
258
- error: 'unsupported_channel',
259
- message: `Outbound verification is only supported for SMS, Telegram, and voice. Got: ${channel}`,
317
+ error: "unsupported_channel",
318
+ message: `Outbound verification is only supported for SMS, Telegram, voice, and Slack. Got: ${channel}`,
260
319
  channel,
261
320
  };
262
321
  }
@@ -271,8 +330,9 @@ function startOutboundSms(
271
330
  if (!rawDestination) {
272
331
  return {
273
332
  success: false,
274
- error: 'missing_destination',
275
- message: 'A destination phone number is required for outbound SMS verification.',
333
+ error: "missing_destination",
334
+ message:
335
+ "A destination phone number is required for outbound SMS verification.",
276
336
  channel,
277
337
  };
278
338
  }
@@ -281,8 +341,9 @@ function startOutboundSms(
281
341
  if (!destination) {
282
342
  return {
283
343
  success: false,
284
- error: 'invalid_destination',
285
- message: 'Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).',
344
+ error: "invalid_destination",
345
+ message:
346
+ "Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).",
286
347
  channel,
287
348
  };
288
349
  }
@@ -291,18 +352,24 @@ function startOutboundSms(
291
352
  if (existingBinding && !rebind) {
292
353
  return {
293
354
  success: false,
294
- error: 'already_bound',
295
- message: 'A guardian is already bound for this channel. Set rebind: true to replace.',
355
+ error: "already_bound",
356
+ message:
357
+ "A guardian is already bound for this channel. Set rebind: true to replace.",
296
358
  channel,
297
359
  };
298
360
  }
299
361
 
300
- const recentSendCount = countRecentSendsToDestination(channel, destination, DESTINATION_RATE_WINDOW_MS);
362
+ const recentSendCount = countRecentSendsToDestination(
363
+ channel,
364
+ destination,
365
+ DESTINATION_RATE_WINDOW_MS,
366
+ );
301
367
  if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
302
368
  return {
303
369
  success: false,
304
- error: 'rate_limited',
305
- message: 'Too many verification attempts to this phone number. Please try again later.',
370
+ error: "rate_limited",
371
+ message:
372
+ "Too many verification attempts to this phone number. Please try again later.",
306
373
  channel,
307
374
  };
308
375
  }
@@ -352,8 +419,9 @@ function startOutboundTelegram(
352
419
  if (!destination) {
353
420
  return {
354
421
  success: false,
355
- error: 'missing_destination',
356
- message: 'A destination (Telegram handle or chat ID) is required for outbound Telegram verification.',
422
+ error: "missing_destination",
423
+ message:
424
+ "A destination (Telegram handle or chat ID) is required for outbound Telegram verification.",
357
425
  channel,
358
426
  };
359
427
  }
@@ -362,20 +430,26 @@ function startOutboundTelegram(
362
430
  if (existingBinding && !rebind) {
363
431
  return {
364
432
  success: false,
365
- error: 'already_bound',
366
- message: 'A guardian is already bound for this channel. Set rebind: true to replace.',
433
+ error: "already_bound",
434
+ message:
435
+ "A guardian is already bound for this channel. Set rebind: true to replace.",
367
436
  channel,
368
437
  };
369
438
  }
370
439
 
371
440
  const normalizedDestination = normalizeTelegramDestination(destination);
372
441
 
373
- const recentSendCount = countRecentSendsToDestination(channel, normalizedDestination, DESTINATION_RATE_WINDOW_MS);
442
+ const recentSendCount = countRecentSendsToDestination(
443
+ channel,
444
+ normalizedDestination,
445
+ DESTINATION_RATE_WINDOW_MS,
446
+ );
374
447
  if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
375
448
  return {
376
449
  success: false,
377
- error: 'rate_limited',
378
- message: 'Too many verification attempts to this destination. Please try again later.',
450
+ error: "rate_limited",
451
+ message:
452
+ "Too many verification attempts to this destination. Please try again later.",
379
453
  channel,
380
454
  };
381
455
  }
@@ -385,8 +459,9 @@ function startOutboundTelegram(
385
459
  if (isNaN(chatIdNum) || chatIdNum < 0) {
386
460
  return {
387
461
  success: false,
388
- error: 'invalid_destination',
389
- message: 'Telegram group chats are not supported for verification. Use a private chat ID or @handle.',
462
+ error: "invalid_destination",
463
+ message:
464
+ "Telegram group chats are not supported for verification. Use a private chat ID or @handle.",
390
465
  channel,
391
466
  };
392
467
  }
@@ -395,7 +470,7 @@ function startOutboundTelegram(
395
470
  assistantId,
396
471
  channel,
397
472
  expectedChatId: destination,
398
- identityBindingStatus: 'bound',
473
+ identityBindingStatus: "bound",
399
474
  destinationAddress: normalizedDestination,
400
475
  });
401
476
 
@@ -411,7 +486,12 @@ function startOutboundTelegram(
411
486
  const nextResendAt = now + RESEND_COOLDOWN_MS;
412
487
  const sendCount = 1;
413
488
 
414
- updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
489
+ updateSessionDelivery(
490
+ sessionResult.sessionId,
491
+ now,
492
+ sendCount,
493
+ nextResendAt,
494
+ );
415
495
  deliverVerificationTelegram(destination, telegramBody, assistantId);
416
496
 
417
497
  return {
@@ -431,19 +511,22 @@ function startOutboundTelegram(
431
511
  if (!botUsername) {
432
512
  return {
433
513
  success: false,
434
- error: 'no_bot_username',
435
- message: 'Telegram bot username is not configured. Set up the Telegram integration first.',
514
+ error: "no_bot_username",
515
+ message:
516
+ "Telegram bot username is not configured. Set up the Telegram integration first.",
436
517
  channel,
437
518
  };
438
519
  }
439
520
 
440
- const bootstrapToken = randomBytes(16).toString('hex');
441
- const bootstrapTokenHash = createHash('sha256').update(bootstrapToken).digest('hex');
521
+ const bootstrapToken = randomBytes(16).toString("hex");
522
+ const bootstrapTokenHash = createHash("sha256")
523
+ .update(bootstrapToken)
524
+ .digest("hex");
442
525
 
443
526
  const sessionResult = createOutboundSession({
444
527
  assistantId,
445
528
  channel,
446
- identityBindingStatus: 'pending_bootstrap',
529
+ identityBindingStatus: "pending_bootstrap",
447
530
  destinationAddress: normalizedDestination,
448
531
  bootstrapTokenHash,
449
532
  });
@@ -470,8 +553,9 @@ function startOutboundVoice(
470
553
  if (!rawDestination) {
471
554
  return {
472
555
  success: false,
473
- error: 'missing_destination',
474
- message: 'A destination phone number is required for outbound voice verification.',
556
+ error: "missing_destination",
557
+ message:
558
+ "A destination phone number is required for outbound voice verification.",
475
559
  channel,
476
560
  };
477
561
  }
@@ -480,8 +564,9 @@ function startOutboundVoice(
480
564
  if (!destination) {
481
565
  return {
482
566
  success: false,
483
- error: 'invalid_destination',
484
- message: 'Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).',
567
+ error: "invalid_destination",
568
+ message:
569
+ "Could not parse phone number. Please enter a valid number (e.g. +15551234567, (555) 123-4567, or 555-123-4567).",
485
570
  channel,
486
571
  };
487
572
  }
@@ -490,18 +575,24 @@ function startOutboundVoice(
490
575
  if (existingBinding && !rebind) {
491
576
  return {
492
577
  success: false,
493
- error: 'already_bound',
494
- message: 'A guardian is already bound for this channel. Set rebind: true to replace.',
578
+ error: "already_bound",
579
+ message:
580
+ "A guardian is already bound for this channel. Set rebind: true to replace.",
495
581
  channel,
496
582
  };
497
583
  }
498
584
 
499
- const recentSendCount = countRecentSendsToDestination(channel, destination, DESTINATION_RATE_WINDOW_MS);
585
+ const recentSendCount = countRecentSendsToDestination(
586
+ channel,
587
+ destination,
588
+ DESTINATION_RATE_WINDOW_MS,
589
+ );
500
590
  if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
501
591
  return {
502
592
  success: false,
503
- error: 'rate_limited',
504
- message: 'Too many verification attempts to this phone number. Please try again later.',
593
+ error: "rate_limited",
594
+ message:
595
+ "Too many verification attempts to this phone number. Please try again later.",
505
596
  channel,
506
597
  };
507
598
  }
@@ -520,7 +611,140 @@ function startOutboundVoice(
520
611
  const sendCount = 1;
521
612
 
522
613
  updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
523
- initiateGuardianVoiceCall(destination, sessionResult.sessionId, assistantId, originConversationId);
614
+ initiateGuardianVoiceCall(
615
+ destination,
616
+ sessionResult.sessionId,
617
+ assistantId,
618
+ originConversationId,
619
+ );
620
+
621
+ return {
622
+ success: true,
623
+ verificationSessionId: sessionResult.sessionId,
624
+ secret: sessionResult.secret,
625
+ expiresAt: sessionResult.expiresAt,
626
+ nextResendAt,
627
+ sendCount,
628
+ channel,
629
+ originConversationId,
630
+ };
631
+ }
632
+
633
+ // ---------------------------------------------------------------------------
634
+ // Slack delivery helper
635
+ // ---------------------------------------------------------------------------
636
+
637
+ /**
638
+ * Deliver a verification Slack DM via the gateway's /deliver/slack endpoint.
639
+ * Fire-and-forget with error logging.
640
+ */
641
+ function deliverVerificationSlack(
642
+ userId: string,
643
+ text: string,
644
+ assistantId: string,
645
+ ): void {
646
+ (async () => {
647
+ try {
648
+ const gatewayUrl = getGatewayInternalBaseUrl();
649
+ const bearerToken = readHttpToken();
650
+ if (!bearerToken) {
651
+ log.error(
652
+ "Cannot deliver verification Slack DM: no runtime HTTP token available",
653
+ );
654
+ return;
655
+ }
656
+ const url = `${gatewayUrl}/deliver/slack`;
657
+ const resp = await fetch(url, {
658
+ method: "POST",
659
+ headers: {
660
+ "Content-Type": "application/json",
661
+ Authorization: `Bearer ${bearerToken}`,
662
+ },
663
+ body: JSON.stringify({ chatId: userId, text, assistantId }),
664
+ });
665
+ if (!resp.ok) {
666
+ const body = await resp.text().catch(() => "<unreadable>");
667
+ log.error(
668
+ { userId, assistantId, status: resp.status, body },
669
+ "Gateway /deliver/slack failed for verification",
670
+ );
671
+ } else {
672
+ log.info({ userId, assistantId }, "Verification Slack DM delivered");
673
+ }
674
+ } catch (err) {
675
+ log.error(
676
+ { err, userId, assistantId },
677
+ "Failed to deliver verification Slack DM",
678
+ );
679
+ }
680
+ })();
681
+ }
682
+
683
+ function startOutboundSlack(
684
+ destination: string | undefined,
685
+ assistantId: string,
686
+ channel: ChannelId,
687
+ rebind?: boolean,
688
+ originConversationId?: string,
689
+ ): OutboundActionResult {
690
+ if (!destination) {
691
+ return {
692
+ success: false,
693
+ error: "missing_destination",
694
+ message: "A Slack user ID is required for outbound Slack verification.",
695
+ channel,
696
+ };
697
+ }
698
+
699
+ const existingBinding = getGuardianBinding(assistantId, channel);
700
+ if (existingBinding && !rebind) {
701
+ return {
702
+ success: false,
703
+ error: "already_bound",
704
+ message:
705
+ "A guardian is already bound for this channel. Set rebind: true to replace.",
706
+ channel,
707
+ };
708
+ }
709
+
710
+ const recentSendCount = countRecentSendsToDestination(
711
+ channel,
712
+ destination,
713
+ DESTINATION_RATE_WINDOW_MS,
714
+ );
715
+ if (recentSendCount >= MAX_SENDS_PER_DESTINATION_WINDOW) {
716
+ return {
717
+ success: false,
718
+ error: "rate_limited",
719
+ message:
720
+ "Too many verification attempts to this Slack user. Please try again later.",
721
+ channel,
722
+ };
723
+ }
724
+
725
+ const sessionResult = createOutboundSession({
726
+ assistantId,
727
+ channel,
728
+ expectedExternalUserId: destination,
729
+ expectedChatId: destination,
730
+ identityBindingStatus: "bound",
731
+ destinationAddress: destination,
732
+ });
733
+
734
+ const slackBody = composeVerificationSlack(
735
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_CHALLENGE_REQUEST,
736
+ {
737
+ code: sessionResult.secret,
738
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
739
+ },
740
+ );
741
+
742
+ const now = Date.now();
743
+ const nextResendAt = now + RESEND_COOLDOWN_MS;
744
+ const sendCount = 1;
745
+
746
+ updateSessionDelivery(sessionResult.sessionId, now, sendCount, nextResendAt);
747
+ deliverVerificationSlack(destination, slackBody, assistantId);
524
748
 
525
749
  return {
526
750
  success: true,
@@ -538,7 +762,9 @@ function startOutboundVoice(
538
762
  // Resend outbound
539
763
  // ---------------------------------------------------------------------------
540
764
 
541
- export function resendOutbound(params: ResendOutboundParams): OutboundActionResult {
765
+ export function resendOutbound(
766
+ params: ResendOutboundParams,
767
+ ): OutboundActionResult {
542
768
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
543
769
  const channel = params.channel;
544
770
  const originConversationId = params.originConversationId;
@@ -547,17 +773,18 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
547
773
  if (!session) {
548
774
  return {
549
775
  success: false,
550
- error: 'no_active_session',
551
- message: 'No active outbound verification session found.',
776
+ error: "no_active_session",
777
+ message: "No active outbound verification session found.",
552
778
  channel,
553
779
  };
554
780
  }
555
781
 
556
- if (session.identityBindingStatus === 'pending_bootstrap') {
782
+ if (session.identityBindingStatus === "pending_bootstrap") {
557
783
  return {
558
784
  success: false,
559
- error: 'pending_bootstrap',
560
- message: 'Cannot resend: waiting for bootstrap deep-link activation. The user must click the link first.',
785
+ error: "pending_bootstrap",
786
+ message:
787
+ "Cannot resend: waiting for bootstrap deep-link activation. The user must click the link first.",
561
788
  channel,
562
789
  };
563
790
  }
@@ -565,8 +792,8 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
565
792
  if (session.nextResendAt != null && Date.now() < session.nextResendAt) {
566
793
  return {
567
794
  success: false,
568
- error: 'rate_limited',
569
- message: 'Please wait before requesting another verification code.',
795
+ error: "rate_limited",
796
+ message: "Please wait before requesting another verification code.",
570
797
  channel,
571
798
  };
572
799
  }
@@ -575,41 +802,52 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
575
802
  if (currentSendCount >= MAX_SENDS_PER_SESSION) {
576
803
  return {
577
804
  success: false,
578
- error: 'max_sends_exceeded',
579
- message: 'Maximum number of verification sends reached for this session.',
805
+ error: "max_sends_exceeded",
806
+ message: "Maximum number of verification sends reached for this session.",
580
807
  channel,
581
808
  };
582
809
  }
583
810
 
584
- const resendDestination = session.destinationAddress ?? session.expectedPhoneE164 ?? session.expectedChatId;
811
+ const resendDestination =
812
+ session.destinationAddress ??
813
+ session.expectedPhoneE164 ??
814
+ session.expectedChatId;
585
815
  if (resendDestination) {
586
- const recentDestSends = countRecentSendsToDestination(channel, resendDestination, DESTINATION_RATE_WINDOW_MS);
816
+ const recentDestSends = countRecentSendsToDestination(
817
+ channel,
818
+ resendDestination,
819
+ DESTINATION_RATE_WINDOW_MS,
820
+ );
587
821
  if (recentDestSends >= MAX_SENDS_PER_DESTINATION_WINDOW) {
588
822
  return {
589
823
  success: false,
590
- error: 'rate_limited',
591
- message: 'Too many verification attempts to this destination. Please try again later.',
824
+ error: "rate_limited",
825
+ message:
826
+ "Too many verification attempts to this destination. Please try again later.",
592
827
  channel,
593
828
  };
594
829
  }
595
830
  }
596
831
 
597
- const destination = session.destinationAddress ?? session.expectedPhoneE164 ?? session.expectedChatId;
832
+ const destination =
833
+ session.destinationAddress ??
834
+ session.expectedPhoneE164 ??
835
+ session.expectedChatId;
598
836
  if (!destination) {
599
837
  return {
600
838
  success: false,
601
- error: 'no_destination',
602
- message: 'Cannot resend: no destination address on the session.',
839
+ error: "no_destination",
840
+ message: "Cannot resend: no destination address on the session.",
603
841
  channel,
604
842
  };
605
843
  }
606
844
 
607
- if (channel === 'telegram') {
845
+ if (channel === "telegram") {
608
846
  const newSession = createOutboundSession({
609
847
  assistantId,
610
848
  channel,
611
849
  expectedChatId: destination,
612
- identityBindingStatus: 'bound',
850
+ identityBindingStatus: "bound",
613
851
  destinationAddress: destination,
614
852
  });
615
853
 
@@ -625,7 +863,12 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
625
863
  const newSendCount = currentSendCount + 1;
626
864
  const nextResendAt = now + RESEND_COOLDOWN_MS;
627
865
 
628
- updateSessionDelivery(newSession.sessionId, now, newSendCount, nextResendAt);
866
+ updateSessionDelivery(
867
+ newSession.sessionId,
868
+ now,
869
+ newSendCount,
870
+ nextResendAt,
871
+ );
629
872
  deliverVerificationTelegram(destination, telegramBody, assistantId);
630
873
 
631
874
  return {
@@ -637,7 +880,7 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
637
880
  channel,
638
881
  originConversationId,
639
882
  };
640
- } else if (channel === 'voice') {
883
+ } else if (channel === "voice") {
641
884
  const newSession = createOutboundSession({
642
885
  assistantId,
643
886
  channel,
@@ -651,8 +894,57 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
651
894
  const newSendCount = currentSendCount + 1;
652
895
  const nextResendAt = now + RESEND_COOLDOWN_MS;
653
896
 
654
- updateSessionDelivery(newSession.sessionId, now, newSendCount, nextResendAt);
655
- initiateGuardianVoiceCall(destination, newSession.sessionId, assistantId, originConversationId);
897
+ updateSessionDelivery(
898
+ newSession.sessionId,
899
+ now,
900
+ newSendCount,
901
+ nextResendAt,
902
+ );
903
+ initiateGuardianVoiceCall(
904
+ destination,
905
+ newSession.sessionId,
906
+ assistantId,
907
+ originConversationId,
908
+ );
909
+
910
+ return {
911
+ success: true,
912
+ verificationSessionId: newSession.sessionId,
913
+ secret: newSession.secret,
914
+ nextResendAt,
915
+ sendCount: newSendCount,
916
+ channel,
917
+ originConversationId,
918
+ };
919
+ } else if (channel === "slack") {
920
+ const newSession = createOutboundSession({
921
+ assistantId,
922
+ channel,
923
+ expectedExternalUserId: destination,
924
+ expectedChatId: destination,
925
+ identityBindingStatus: "bound",
926
+ destinationAddress: destination,
927
+ });
928
+
929
+ const slackBody = composeVerificationSlack(
930
+ GUARDIAN_VERIFY_TEMPLATE_KEYS.SLACK_RESEND,
931
+ {
932
+ code: newSession.secret,
933
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
934
+ },
935
+ );
936
+
937
+ const now = Date.now();
938
+ const newSendCount = currentSendCount + 1;
939
+ const nextResendAt = now + RESEND_COOLDOWN_MS;
940
+
941
+ updateSessionDelivery(
942
+ newSession.sessionId,
943
+ now,
944
+ newSendCount,
945
+ nextResendAt,
946
+ );
947
+ deliverVerificationSlack(destination, slackBody, assistantId);
656
948
 
657
949
  return {
658
950
  success: true,
@@ -674,13 +966,10 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
674
966
  destinationAddress: destination,
675
967
  });
676
968
 
677
- const smsBody = composeVerificationSms(
678
- GUARDIAN_VERIFY_TEMPLATE_KEYS.RESEND,
679
- {
680
- code: newSession.secret,
681
- expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
682
- },
683
- );
969
+ const smsBody = composeVerificationSms(GUARDIAN_VERIFY_TEMPLATE_KEYS.RESEND, {
970
+ code: newSession.secret,
971
+ expiresInMinutes: Math.floor(SESSION_TTL_SECONDS / 60),
972
+ });
684
973
 
685
974
  const now = Date.now();
686
975
  const newSendCount = currentSendCount + 1;
@@ -704,7 +993,9 @@ export function resendOutbound(params: ResendOutboundParams): OutboundActionResu
704
993
  // Cancel outbound
705
994
  // ---------------------------------------------------------------------------
706
995
 
707
- export function cancelOutbound(params: CancelOutboundParams): OutboundActionResult {
996
+ export function cancelOutbound(
997
+ params: CancelOutboundParams,
998
+ ): OutboundActionResult {
708
999
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
709
1000
  const channel = params.channel;
710
1001
 
@@ -712,13 +1003,13 @@ export function cancelOutbound(params: CancelOutboundParams): OutboundActionResu
712
1003
  if (!session) {
713
1004
  return {
714
1005
  success: false,
715
- error: 'no_active_session',
716
- message: 'No active outbound verification session found.',
1006
+ error: "no_active_session",
1007
+ message: "No active outbound verification session found.",
717
1008
  channel,
718
1009
  };
719
1010
  }
720
1011
 
721
- updateSessionStatus(session.id, 'revoked');
1012
+ updateSessionStatus(session.id, "revoked");
722
1013
 
723
1014
  return {
724
1015
  success: true,