@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
@@ -1,18 +1,23 @@
1
1
  /**
2
2
  * Telegram channel invite adapter.
3
3
  *
4
- * Builds `https://t.me/<botUsername>?start=iv_<token>` deep links and
5
- * extracts invite tokens from `/start iv_<token>` command payloads.
4
+ * Builds `https://t.me/<botUsername>?start=iv_<token>` deep links,
5
+ * extracts invite tokens from `/start iv_<token>` command payloads,
6
+ * and resolves the bot's channel handle for invite instructions.
6
7
  *
7
8
  * The `iv_` prefix distinguishes invite tokens from `gv_` (guardian
8
9
  * verification) tokens that use the same `/start` deep-link mechanism.
9
10
  */
10
11
 
11
12
  import type { ChannelId } from "../../channels/types.js";
12
- import { getCredentialMetadata } from "../../tools/credentials/metadata-store.js";
13
+ import { getSecureKey } from "../../security/secure-keys.js";
14
+ import {
15
+ getCredentialMetadata,
16
+ upsertCredentialMetadata,
17
+ } from "../../tools/credentials/metadata-store.js";
18
+ import { getLogger } from "../../util/logger.js";
13
19
  import type {
14
20
  ChannelInviteAdapter,
15
- GuardianInstruction,
16
21
  InviteShareLink,
17
22
  } from "../channel-invite-transport.js";
18
23
 
@@ -37,6 +42,66 @@ function getTelegramBotUsername(): string | undefined {
37
42
  return process.env.TELEGRAM_BOT_USERNAME || undefined;
38
43
  }
39
44
 
45
+ /**
46
+ * Ensure the Telegram bot username is resolved and cached in credential
47
+ * metadata. When the bot token was configured via CLI `credential set`,
48
+ * `credential_store` tool, or ingress secret redirect, the `getMe` API
49
+ * call that populates `accountInfo` is skipped — this function fills that
50
+ * gap so that invite share links can be generated.
51
+ */
52
+ export async function ensureTelegramBotUsernameResolved(): Promise<void> {
53
+ const meta = getCredentialMetadata("telegram", "bot_token");
54
+ if (
55
+ meta?.accountInfo &&
56
+ typeof meta.accountInfo === "string" &&
57
+ meta.accountInfo.trim().length > 0
58
+ ) {
59
+ return; // Username already cached
60
+ }
61
+
62
+ const token = getSecureKey("credential:telegram:bot_token");
63
+ if (!token) return;
64
+
65
+ try {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), 5_000);
68
+ let res: Response;
69
+ try {
70
+ res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
71
+ signal: controller.signal,
72
+ });
73
+ } finally {
74
+ clearTimeout(timeout);
75
+ }
76
+ if (!res.ok) {
77
+ getLogger("telegram-invite").warn(
78
+ "Failed to resolve Telegram bot username: HTTP %d",
79
+ res.status,
80
+ );
81
+ return;
82
+ }
83
+ const body = (await res.json()) as {
84
+ ok: boolean;
85
+ result?: { username?: string };
86
+ };
87
+ const username = body.result?.username;
88
+ if (!username) {
89
+ getLogger("telegram-invite").warn(
90
+ "Telegram getMe response did not include a username",
91
+ );
92
+ return;
93
+ }
94
+ upsertCredentialMetadata("telegram", "bot_token", {
95
+ accountInfo: username,
96
+ });
97
+ } catch (err) {
98
+ getLogger("telegram-invite").warn(
99
+ { err },
100
+ "Failed to resolve Telegram bot username via getMe API",
101
+ );
102
+ }
103
+ }
104
+
40
105
  // ---------------------------------------------------------------------------
41
106
  // Token prefix
42
107
  // ---------------------------------------------------------------------------
@@ -68,23 +133,6 @@ export const telegramInviteAdapter: ChannelInviteAdapter = {
68
133
  };
69
134
  },
70
135
 
71
- buildGuardianInstruction(params: {
72
- inviteCode: string;
73
- contactName?: string;
74
- }): GuardianInstruction {
75
- const botUsername = getTelegramBotUsername();
76
- const contactLabel = params.contactName || "the contact";
77
- if (!botUsername) {
78
- return {
79
- instruction: `Tell ${contactLabel} to message the assistant on Telegram and provide the code ${params.inviteCode}.`,
80
- };
81
- }
82
- return {
83
- instruction: `Tell ${contactLabel} to message @${botUsername} on Telegram and provide the code ${params.inviteCode}.`,
84
- channelHandle: `@${botUsername}`,
85
- };
86
- },
87
-
88
136
  resolveChannelHandle(): string | undefined {
89
137
  const botUsername = getTelegramBotUsername();
90
138
  if (!botUsername) return undefined;
@@ -126,10 +174,3 @@ export const telegramInviteAdapter: ChannelInviteAdapter = {
126
174
  return undefined;
127
175
  },
128
176
  };
129
-
130
- // ---------------------------------------------------------------------------
131
- // Backward-compatible alias
132
- // ---------------------------------------------------------------------------
133
-
134
- /** @deprecated Use `telegramInviteAdapter` instead. */
135
- export const telegramInviteTransport = telegramInviteAdapter;
@@ -49,10 +49,3 @@ export const voiceInviteAdapter: ChannelInviteAdapter = {
49
49
  return undefined;
50
50
  },
51
51
  };
52
-
53
- // ---------------------------------------------------------------------------
54
- // Backward-compatible alias
55
- // ---------------------------------------------------------------------------
56
-
57
- /** @deprecated Use `voiceInviteAdapter` instead. */
58
- export const voiceInviteTransport = voiceInviteAdapter;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WhatsApp channel invite adapter.
3
+ *
4
+ * WhatsApp uses Meta WhatsApp Business API credentials, not Twilio.
5
+ * The Meta API identifies numbers by phone_number_id (a numeric string),
6
+ * which isn't a user-facing phone number. The display number is resolved
7
+ * from workspace config (`whatsapp.phoneNumber`), falling back to
8
+ * `undefined` (triggering generic instructions) when not configured.
9
+ */
10
+
11
+ import type { ChannelId } from "../../channels/types.js";
12
+ import { getConfig } from "../../config/loader.js";
13
+ import type { ChannelInviteAdapter } from "../channel-invite-transport.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Phone number resolution
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Resolve the user-configured WhatsApp display phone number from workspace
21
+ * config. The Meta API's `phone_number_id` is not user-facing, so the
22
+ * display number must be explicitly configured by the user.
23
+ */
24
+ export function resolveWhatsAppDisplayNumber(): string | undefined {
25
+ try {
26
+ const config = getConfig();
27
+ return config.whatsapp.phoneNumber || undefined;
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Adapter implementation
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export const whatsappInviteAdapter: ChannelInviteAdapter = {
38
+ channel: "whatsapp" as ChannelId,
39
+
40
+ resolveChannelHandle(): string | undefined {
41
+ return resolveWhatsAppDisplayNumber();
42
+ },
43
+ };
@@ -3,9 +3,12 @@ import {
3
3
  getTollFreeVerificationStatus,
4
4
  hasTwilioCredentials,
5
5
  } from "../calls/twilio-rest.js";
6
+ import { getChannelInvitePolicy } from "../channels/config.js";
6
7
  import { getTwilioPhoneNumberEnv } from "../config/env.js";
7
8
  import { loadRawConfig } from "../config/loader.js";
9
+ import { getEmailService } from "../email/service.js";
8
10
  import { getSecureKey } from "../security/secure-keys.js";
11
+ import { resolveWhatsAppDisplayNumber } from "./channel-invite-transports/whatsapp.js";
9
12
  import type {
10
13
  ChannelId,
11
14
  ChannelProbe,
@@ -19,25 +22,13 @@ export const REMOTE_TTL_MS = 5 * 60 * 1000;
19
22
 
20
23
  // ── SMS Probe ───────────────────────────────────────────────────────────────
21
24
 
22
- function hasIngressConfigured(): boolean {
23
- try {
24
- const raw = loadRawConfig();
25
- const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
26
- const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
27
- const enabled =
28
- (ingress.enabled as boolean | undefined) ??
29
- (publicBaseUrl ? true : false);
30
- return enabled && publicBaseUrl.length > 0;
31
- } catch {
32
- return false;
33
- }
25
+ // Keep Twilio phone-number resolution inside an already-authorized module so
26
+ // the secure-key import boundary does not expand for shared config helpers.
27
+ function resolveSmsPhoneNumber(): string {
28
+ return resolveTwilioPhoneNumber();
34
29
  }
35
30
 
36
- /**
37
- * Resolve SMS from-number with canonical precedence:
38
- * env override -> config sms.phoneNumber -> secure key fallback.
39
- */
40
- function resolveSmsPhoneNumber(): string {
31
+ function resolveTwilioPhoneNumber(): string {
41
32
  try {
42
33
  const raw = loadRawConfig();
43
34
  const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
@@ -56,6 +47,20 @@ function resolveSmsPhoneNumber(): string {
56
47
  }
57
48
  }
58
49
 
50
+ function hasIngressConfigured(): boolean {
51
+ try {
52
+ const raw = loadRawConfig();
53
+ const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
54
+ const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
55
+ const enabled =
56
+ (ingress.enabled as boolean | undefined) ??
57
+ (publicBaseUrl ? true : false);
58
+ return enabled && publicBaseUrl.length > 0;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
59
64
  const smsProbe: ChannelProbe = {
60
65
  channel: "sms",
61
66
  runLocalChecks(): ReadinessCheckResult[] {
@@ -171,32 +176,6 @@ const smsProbe: ChannelProbe = {
171
176
 
172
177
  // ── Voice Probe ─────────────────────────────────────────────────────────────
173
178
 
174
- /**
175
- * Resolve voice from-number with the same precedence as SMS:
176
- * env override -> config sms.phoneNumber -> secure key fallback.
177
- *
178
- * Voice and SMS share the same Twilio phone number infrastructure, so the
179
- * resolution logic is identical to resolveSmsPhoneNumber.
180
- */
181
- function resolveVoicePhoneNumber(): string {
182
- try {
183
- const raw = loadRawConfig();
184
- const smsConfig = (raw?.sms ?? {}) as Record<string, unknown>;
185
- return (
186
- getTwilioPhoneNumberEnv() ||
187
- (smsConfig.phoneNumber as string) ||
188
- getSecureKey("credential:twilio:phone_number") ||
189
- ""
190
- );
191
- } catch {
192
- return (
193
- getTwilioPhoneNumberEnv() ||
194
- getSecureKey("credential:twilio:phone_number") ||
195
- ""
196
- );
197
- }
198
- }
199
-
200
179
  const voiceProbe: ChannelProbe = {
201
180
  channel: "voice",
202
181
  runLocalChecks(): ReadinessCheckResult[] {
@@ -211,7 +190,7 @@ const voiceProbe: ChannelProbe = {
211
190
  : "Twilio Account SID and Auth Token are not configured",
212
191
  });
213
192
 
214
- const resolvedNumber = resolveVoicePhoneNumber();
193
+ const resolvedNumber = resolveSmsPhoneNumber();
215
194
  const hasPhone = !!resolvedNumber;
216
195
  results.push({
217
196
  name: "phone_number",
@@ -275,6 +254,181 @@ const telegramProbe: ChannelProbe = {
275
254
  // Telegram has no remote checks currently
276
255
  };
277
256
 
257
+ // ── Email Probe ─────────────────────────────────────────────────────────────
258
+
259
+ const emailProbe: ChannelProbe = {
260
+ channel: "email",
261
+ runLocalChecks(): ReadinessCheckResult[] {
262
+ const results: ReadinessCheckResult[] = [];
263
+
264
+ const hasApiKey = !!(
265
+ getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
266
+ );
267
+ results.push({
268
+ name: "agentmail_api_key",
269
+ passed: hasApiKey,
270
+ message: hasApiKey
271
+ ? "AgentMail API key is configured"
272
+ : "AgentMail API key is not configured",
273
+ });
274
+
275
+ const invitePolicy = getChannelInvitePolicy("email");
276
+ results.push({
277
+ name: "invite_policy",
278
+ passed: invitePolicy.codeRedemptionEnabled,
279
+ message: invitePolicy.codeRedemptionEnabled
280
+ ? "Email invite code redemption is enabled"
281
+ : "Email invite code redemption is disabled",
282
+ });
283
+
284
+ const hasIngress = hasIngressConfigured();
285
+ results.push({
286
+ name: "ingress",
287
+ passed: hasIngress,
288
+ message: hasIngress
289
+ ? "Public ingress URL is configured"
290
+ : "Public ingress URL is not configured or disabled",
291
+ });
292
+
293
+ return results;
294
+ },
295
+ async runRemoteChecks(): Promise<ReadinessCheckResult[]> {
296
+ // Only worth checking if the API key is present
297
+ const hasApiKey = !!(
298
+ getSecureKey("agentmail") || getSecureKey("credential:agentmail:api_key")
299
+ );
300
+ if (!hasApiKey) return [];
301
+
302
+ try {
303
+ const address = await getEmailService().getPrimaryInboxAddress();
304
+ const hasInbox = !!address;
305
+ return [
306
+ {
307
+ name: "inbox_configured",
308
+ passed: hasInbox,
309
+ message: hasInbox
310
+ ? `Inbox address is configured (${address})`
311
+ : "No inbox address configured — create one with: vellum email setup inboxes",
312
+ },
313
+ ];
314
+ } catch (err) {
315
+ const message = err instanceof Error ? err.message : String(err);
316
+ return [
317
+ {
318
+ name: "inbox_configured",
319
+ passed: false,
320
+ message: `Failed to check inbox configuration: ${message}`,
321
+ },
322
+ ];
323
+ }
324
+ },
325
+ };
326
+
327
+ // ── WhatsApp Probe ──────────────────────────────────────────────────────────
328
+
329
+ const whatsappProbe: ChannelProbe = {
330
+ channel: "whatsapp",
331
+ runLocalChecks(): ReadinessCheckResult[] {
332
+ const results: ReadinessCheckResult[] = [];
333
+
334
+ const hasPhoneNumberId = !!getSecureKey(
335
+ "credential:whatsapp:phone_number_id",
336
+ );
337
+ results.push({
338
+ name: "whatsapp_phone_number_id",
339
+ passed: hasPhoneNumberId,
340
+ message: hasPhoneNumberId
341
+ ? "WhatsApp phone number ID is configured"
342
+ : "WhatsApp phone number ID is not configured",
343
+ });
344
+
345
+ const hasAccessToken = !!getSecureKey("credential:whatsapp:access_token");
346
+ results.push({
347
+ name: "whatsapp_access_token",
348
+ passed: hasAccessToken,
349
+ message: hasAccessToken
350
+ ? "WhatsApp access token is configured"
351
+ : "WhatsApp access token is not configured",
352
+ });
353
+
354
+ const hasAppSecret = !!getSecureKey("credential:whatsapp:app_secret");
355
+ results.push({
356
+ name: "whatsapp_app_secret",
357
+ passed: hasAppSecret,
358
+ message: hasAppSecret
359
+ ? "WhatsApp app secret is configured"
360
+ : "WhatsApp app secret is not configured",
361
+ });
362
+
363
+ const hasWebhookVerifyToken = !!getSecureKey(
364
+ "credential:whatsapp:webhook_verify_token",
365
+ );
366
+ results.push({
367
+ name: "whatsapp_webhook_verify_token",
368
+ passed: hasWebhookVerifyToken,
369
+ message: hasWebhookVerifyToken
370
+ ? "WhatsApp webhook verify token is configured"
371
+ : "WhatsApp webhook verify token is not configured",
372
+ });
373
+
374
+ const displayNumber = resolveWhatsAppDisplayNumber();
375
+ const hasDisplayNumber = !!displayNumber;
376
+ results.push({
377
+ name: "whatsapp_display_phone_number",
378
+ passed: hasDisplayNumber,
379
+ message: hasDisplayNumber
380
+ ? `WhatsApp display phone number is configured (${displayNumber})`
381
+ : "WhatsApp display phone number is not configured — set whatsapp.phoneNumber in workspace config",
382
+ });
383
+
384
+ const invitePolicy = getChannelInvitePolicy("whatsapp");
385
+ results.push({
386
+ name: "invite_policy",
387
+ passed: invitePolicy.codeRedemptionEnabled,
388
+ message: invitePolicy.codeRedemptionEnabled
389
+ ? "WhatsApp invite code redemption is enabled"
390
+ : "WhatsApp invite code redemption is disabled",
391
+ });
392
+
393
+ const hasIngress = hasIngressConfigured();
394
+ results.push({
395
+ name: "ingress",
396
+ passed: hasIngress,
397
+ message: hasIngress
398
+ ? "Public ingress URL is configured"
399
+ : "Public ingress URL is not configured or disabled",
400
+ });
401
+
402
+ return results;
403
+ },
404
+ };
405
+
406
+ // ── Slack Probe ─────────────────────────────────────────────────────────────
407
+
408
+ const slackProbe: ChannelProbe = {
409
+ channel: "slack",
410
+ runLocalChecks(): ReadinessCheckResult[] {
411
+ const hasBotToken = !!getSecureKey("credential:slack_channel:bot_token");
412
+ const hasAppToken = !!getSecureKey("credential:slack_channel:app_token");
413
+ return [
414
+ {
415
+ name: "bot_token",
416
+ passed: hasBotToken,
417
+ message: hasBotToken
418
+ ? "Slack bot token is configured"
419
+ : "Slack bot token is not configured",
420
+ },
421
+ {
422
+ name: "app_token",
423
+ passed: hasAppToken,
424
+ message: hasAppToken
425
+ ? "Slack app token is configured"
426
+ : "Slack app token is not configured",
427
+ },
428
+ ];
429
+ },
430
+ };
431
+
278
432
  // ── Service ─────────────────────────────────────────────────────────────────
279
433
 
280
434
  export class ChannelReadinessService {
@@ -416,11 +570,14 @@ export class ChannelReadinessService {
416
570
 
417
571
  // ── Factory ─────────────────────────────────────────────────────────────────
418
572
 
419
- /** Create a service instance with built-in SMS, Voice, and Telegram probes registered. */
573
+ /** Create a service instance with built-in SMS, Voice, Telegram, Email, WhatsApp, and Slack probes registered. */
420
574
  export function createReadinessService(): ChannelReadinessService {
421
575
  const service = new ChannelReadinessService();
422
576
  service.registerProbe(smsProbe);
423
577
  service.registerProbe(voiceProbe);
424
578
  service.registerProbe(telegramProbe);
579
+ service.registerProbe(emailProbe);
580
+ service.registerProbe(whatsappProbe);
581
+ service.registerProbe(slackProbe);
425
582
  return service;
426
583
  }
@@ -18,6 +18,7 @@ import {
18
18
  createCanonicalGuardianDelivery,
19
19
  } from "../memory/canonical-guardian-store.js";
20
20
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
21
+ import type { NotificationSourceChannel } from "../notifications/signal.js";
21
22
  import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
22
23
  import { getLogger } from "../util/logger.js";
23
24
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
@@ -144,7 +145,7 @@ export function bridgeConfirmationRequestToGuardian(
144
145
  // Emit guardian.question notification so the guardian is alerted.
145
146
  const signalPromise = emitNotificationSignal({
146
147
  sourceEventName: "guardian.question",
147
- sourceChannel,
148
+ sourceChannel: sourceChannel as NotificationSourceChannel,
148
149
  sourceSessionId: conversationId,
149
150
  attentionHints: {
150
151
  requiresAction: true,
@@ -259,9 +259,9 @@ function initiateGuardianVoiceCall(
259
259
  // Start outbound
260
260
  // ---------------------------------------------------------------------------
261
261
 
262
- export function startOutbound(
262
+ export async function startOutbound(
263
263
  params: StartOutboundParams,
264
- ): OutboundActionResult {
264
+ ): Promise<OutboundActionResult> {
265
265
  const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
266
266
  const channel = params.channel;
267
267
  const originConversationId = params.originConversationId;
@@ -275,7 +275,7 @@ export function startOutbound(
275
275
  originConversationId,
276
276
  );
277
277
  } else if (channel === "telegram") {
278
- return startOutboundTelegram(
278
+ return await startOutboundTelegram(
279
279
  params.destination,
280
280
  assistantId,
281
281
  channel,
@@ -397,13 +397,13 @@ function startOutboundSms(
397
397
  };
398
398
  }
399
399
 
400
- function startOutboundTelegram(
400
+ async function startOutboundTelegram(
401
401
  destination: string | undefined,
402
402
  assistantId: string,
403
403
  channel: ChannelId,
404
404
  rebind?: boolean,
405
405
  originConversationId?: string,
406
- ): OutboundActionResult {
406
+ ): Promise<OutboundActionResult> {
407
407
  if (!destination) {
408
408
  return {
409
409
  success: false,
@@ -495,6 +495,9 @@ function startOutboundTelegram(
495
495
  }
496
496
 
497
497
  // Telegram handle/username: create a pending_bootstrap session with deep-link
498
+ const { ensureTelegramBotUsernameResolved } =
499
+ await import("./channel-invite-transports/telegram.js");
500
+ await ensureTelegramBotUsernameResolved();
498
501
  const botUsername = getTelegramBotUsername();
499
502
  if (!botUsername) {
500
503
  return {
@@ -130,6 +130,7 @@ import { surfaceActionRouteDefinitions } from "./routes/surface-action-routes.js
130
130
  import { surfaceContentRouteDefinitions } from "./routes/surface-content-routes.js";
131
131
  import { trustRulesRouteDefinitions } from "./routes/trust-rules-routes.js";
132
132
  import { twilioRouteDefinitions } from "./routes/twilio-routes.js";
133
+ import { usageRouteDefinitions } from "./routes/usage-routes.js";
133
134
 
134
135
  // Re-export for consumers
135
136
  export { isPrivateAddress } from "./middleware/auth.js";
@@ -684,6 +685,7 @@ export class RuntimeHttpServer {
684
685
  ...secretRouteDefinitions(),
685
686
  ...identityRouteDefinitions(),
686
687
  ...debugRouteDefinitions(),
688
+ ...usageRouteDefinitions(),
687
689
 
688
690
  // Browser relay — not extracted into a domain module because
689
691
  // these two routes depend on the in-process extensionRelayServer