@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
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Generative invite instructions.
3
+ *
4
+ * Uses the configured provider to generate a short, first-person instruction
5
+ * telling the guardian how to invite a contact via a specific channel. Falls
6
+ * back to a deterministic template when the provider is unavailable or
7
+ * generation fails/times out.
8
+ */
9
+
10
+ import {
11
+ createTimeout,
12
+ extractText,
13
+ resolveConfiguredProvider,
14
+ userMessage,
15
+ } from "../providers/provider-send-message.js";
16
+ import { getLogger } from "../util/logger.js";
17
+
18
+ const log = getLogger("invite-instruction-generator");
19
+
20
+ /** Timeout for the generative instruction call (ms). */
21
+ const GENERATION_TIMEOUT_MS = 5_000;
22
+
23
+ /** Maximum allowed length for a generated instruction. */
24
+ const MAX_INSTRUCTION_LENGTH = 500;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Channel display label
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** Human-readable label for a channel type. */
31
+ export function channelDisplayLabel(type: string): string {
32
+ switch (type) {
33
+ case "telegram":
34
+ return "Telegram";
35
+ case "sms":
36
+ return "SMS";
37
+ case "email":
38
+ return "Email";
39
+ case "slack":
40
+ return "Slack";
41
+ case "voice":
42
+ return "Voice";
43
+ default:
44
+ return type.charAt(0).toUpperCase() + type.slice(1);
45
+ }
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Deterministic fallback
50
+ // ---------------------------------------------------------------------------
51
+
52
+ /**
53
+ * Build a deterministic fallback instruction when the LLM is unavailable.
54
+ */
55
+ export function buildFallbackInstruction(params: {
56
+ contactName?: string;
57
+ channelLabel: string;
58
+ channelHandle?: string;
59
+ shareUrl?: string;
60
+ }): string {
61
+ const contact = params.contactName || "the contact";
62
+ const handle = params.channelHandle
63
+ ? ` at ${params.channelHandle}`
64
+ : ` on ${params.channelLabel}`;
65
+ if (params.shareUrl) {
66
+ return `Send ${contact} this link: ${params.shareUrl} — or tell them to message me${handle} with the code below.`;
67
+ }
68
+ return `Tell ${contact} to message me${handle} with the code below.`;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // LLM-powered generation
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * Generate an invite instruction via the configured LLM provider. Returns a
77
+ * deterministic fallback when the provider is unavailable, generation times
78
+ * out (5s), or the output fails validation.
79
+ */
80
+ export async function generateInviteInstruction(params: {
81
+ contactName?: string;
82
+ channelType: string;
83
+ channelHandle?: string;
84
+ /** Whether a share URL is available (shown separately in the UI). */
85
+ hasShareUrl?: boolean;
86
+ /**
87
+ * Actual share URL for the deterministic fallback only. Never sent to the
88
+ * LLM — the URL contains the raw invite token which is a redemption
89
+ * credential.
90
+ */
91
+ shareUrl?: string;
92
+ }): Promise<string> {
93
+ const channelLabel = channelDisplayLabel(params.channelType);
94
+ const fallback = buildFallbackInstruction({
95
+ contactName: params.contactName,
96
+ channelLabel,
97
+ channelHandle: params.channelHandle,
98
+ shareUrl: params.shareUrl,
99
+ });
100
+
101
+ const resolved = resolveConfiguredProvider();
102
+ if (!resolved) {
103
+ log.debug(
104
+ "No provider available for invite instruction generation, using fallback",
105
+ );
106
+ return fallback;
107
+ }
108
+
109
+ const { signal, cleanup } = createTimeout(GENERATION_TIMEOUT_MS);
110
+
111
+ try {
112
+ const parts: string[] = [
113
+ "Generate a 1–2 sentence instruction from the assistant's perspective telling the user how to invite a contact.",
114
+ "",
115
+ `Channel: ${channelLabel}`,
116
+ ];
117
+ if (params.contactName) {
118
+ parts.push(`Contact name: ${params.contactName}`);
119
+ }
120
+ if (params.channelHandle) {
121
+ parts.push(`Channel handle: ${params.channelHandle}`);
122
+ }
123
+ if (params.hasShareUrl) {
124
+ parts.push("A share link is available (displayed separately in the UI).");
125
+ } else {
126
+ parts.push(
127
+ "No share link is available for this channel. Do NOT mention sharing a link or URL.",
128
+ );
129
+ }
130
+ parts.push(
131
+ "",
132
+ "Requirements:",
133
+ '- Write from the assistant\'s perspective using first person ("message me"), NOT third person ("message the assistant").',
134
+ "- Do NOT include the invite code — it is displayed separately in the UI.",
135
+ );
136
+ if (params.hasShareUrl) {
137
+ parts.push(
138
+ "- When a share link is available, mention that the user can share the link.",
139
+ );
140
+ }
141
+ parts.push(
142
+ "- Keep the instruction concise (1–2 sentences, under 500 characters).",
143
+ '- Refer to the invite code as "the code below" since it is shown beneath this instruction.',
144
+ "",
145
+ "Respond with the instruction text only — no labels, no extra formatting.",
146
+ );
147
+
148
+ const prompt = parts.join("\n");
149
+
150
+ const response = await resolved.provider.sendMessage(
151
+ [userMessage(prompt)],
152
+ undefined,
153
+ undefined,
154
+ { signal, config: { modelIntent: "latency-optimized" } },
155
+ );
156
+
157
+ const text = extractText(response).trim();
158
+
159
+ if (!text || text.length > MAX_INSTRUCTION_LENGTH) {
160
+ log.warn(
161
+ { length: text.length },
162
+ "Generated invite instruction failed validation, using fallback",
163
+ );
164
+ return fallback;
165
+ }
166
+
167
+ return text;
168
+ } catch (err) {
169
+ if (signal.aborted) {
170
+ log.warn("Invite instruction generation timed out, using fallback");
171
+ } else {
172
+ log.warn({ err }, "Invite instruction generation failed, using fallback");
173
+ }
174
+ return fallback;
175
+ } finally {
176
+ cleanup();
177
+ }
178
+ }
@@ -25,7 +25,11 @@ import {
25
25
  } from "../memory/invite-store.js";
26
26
  import { isValidE164 } from "../util/phone.js";
27
27
  import { generateVoiceCode, hashVoiceCode } from "../util/voice-code.js";
28
- import { getInviteAdapterRegistry } from "./channel-invite-transport.js";
28
+ import {
29
+ getInviteAdapterRegistry,
30
+ resolveAdapterHandle,
31
+ } from "./channel-invite-transport.js";
32
+ import { generateInviteInstruction } from "./invite-instruction-generator.js";
29
33
  import {
30
34
  type InviteRedemptionOutcome,
31
35
  redeemInvite as redeemInviteTyped,
@@ -140,19 +144,19 @@ export type IngressResult<T> =
140
144
  // Invite operations
141
145
  // ---------------------------------------------------------------------------
142
146
 
143
- export function createIngressInvite(params: {
147
+ export async function createIngressInvite(params: {
144
148
  sourceChannel?: string;
145
149
  note?: string;
146
150
  maxUses?: number;
147
151
  expiresInMs?: number;
148
- // Contact display name for personalizing guardian instructions
152
+ // Contact display name for personalizing invite instructions
149
153
  contactName?: string;
150
154
  // Voice invite parameters
151
155
  expectedExternalUserId?: string;
152
156
  voiceCodeDigits?: number;
153
157
  friendName?: string;
154
158
  guardianName?: string;
155
- }): IngressResult<InviteResponseData> {
159
+ }): Promise<IngressResult<InviteResponseData>> {
156
160
  if (!params.sourceChannel) {
157
161
  return { ok: false, error: "sourceChannel is required for create" };
158
162
  }
@@ -220,7 +224,7 @@ export function createIngressInvite(params: {
220
224
  : { inviteCodeHash }),
221
225
  });
222
226
 
223
- // Build guardian instruction for non-voice invites
227
+ // Build invite instruction for non-voice invites via LLM generation
224
228
  let guardianInstruction: string | undefined;
225
229
  let channelHandle: string | undefined;
226
230
  if (!isVoice && inviteCode) {
@@ -230,27 +234,20 @@ export function createIngressInvite(params: {
230
234
  const adapter = channelId
231
235
  ? getInviteAdapterRegistry().get(channelId)
232
236
  : undefined;
233
-
234
- if (adapter?.buildGuardianInstruction) {
235
- try {
236
- const adapterResult = adapter.buildGuardianInstruction({
237
- inviteCode,
238
- contactName: params.contactName,
239
- });
240
- if (adapterResult) {
241
- guardianInstruction = adapterResult.instruction;
242
- channelHandle = adapterResult.channelHandle;
243
- }
244
- } catch {
245
- // Fall through to generic instruction if adapter fails
246
- }
247
- }
248
-
249
- if (!guardianInstruction) {
250
- const contactLabel = params.contactName || "the contact";
251
- const channelLabel = params.sourceChannel;
252
- guardianInstruction = `Tell ${contactLabel} to contact the assistant on ${channelLabel} and provide the code ${inviteCode}.`;
237
+ if (params.sourceChannel === "telegram") {
238
+ const { ensureTelegramBotUsernameResolved } =
239
+ await import("./channel-invite-transports/telegram.js");
240
+ await ensureTelegramBotUsernameResolved();
253
241
  }
242
+ channelHandle = adapter ? await resolveAdapterHandle(adapter) : undefined;
243
+ const share = buildSharePayload(params.sourceChannel, rawToken);
244
+ guardianInstruction = await generateInviteInstruction({
245
+ contactName: params.contactName,
246
+ channelType: params.sourceChannel,
247
+ channelHandle,
248
+ hasShareUrl: !!share?.url,
249
+ shareUrl: share?.url,
250
+ });
254
251
  }
255
252
 
256
253
  // Voice invites must not expose the token — callers must redeem via the
@@ -40,6 +40,13 @@ export interface TransportConfig {
40
40
  authHeader?: string;
41
41
  /** Header name for auth. Defaults to "Authorization" for runtime, "X-Session-Token" for managed. */
42
42
  authHeaderName?: string;
43
+ /**
44
+ * Additional headers to include with every request.
45
+ * Merged after Content-Type but before auth-header injection,
46
+ * so an explicit auth header from the transport always wins
47
+ * over a same-named entry in defaultHeaders.
48
+ */
49
+ defaultHeaders?: Record<string, string>;
43
50
  /** Custom fetch implementation for testing or environments without global fetch. */
44
51
  fetchFn?: typeof fetch;
45
52
  }
@@ -277,6 +284,12 @@ function buildHeaders(
277
284
  headers["Content-Type"] = contentType;
278
285
  }
279
286
 
287
+ // Merge defaultHeaders after Content-Type but before auth injection
288
+ if (config.defaultHeaders) {
289
+ Object.assign(headers, config.defaultHeaders);
290
+ }
291
+
292
+ // Auth header is applied last so it always wins over defaultHeaders
280
293
  if (config.authHeader) {
281
294
  const headerName =
282
295
  config.authHeaderName ??
@@ -167,7 +167,7 @@ export function handleDownloadSharedApp(shareToken: string): Response {
167
167
  return new Response(new Uint8Array(record.bundleData), {
168
168
  headers: {
169
169
  "Content-Type": "application/zip",
170
- "Content-Disposition": 'attachment; filename="app.vellumapp"',
170
+ "Content-Disposition": 'attachment; filename="app.vellum"',
171
171
  },
172
172
  });
173
173
  }
@@ -11,6 +11,7 @@ import {
11
11
  type GuardianApprovalRequest,
12
12
  } from "../../../memory/channel-guardian-store.js";
13
13
  import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
14
+ import type { NotificationSourceChannel } from "../../../notifications/signal.js";
14
15
  import { getLogger } from "../../../util/logger.js";
15
16
  import { runApprovalConversationTurn } from "../../approval-conversation-turn.js";
16
17
  import { composeApprovalMessageGenerative } from "../../approval-message-composer.js";
@@ -791,7 +792,7 @@ async function handleAccessRequestApproval(
791
792
  // Emit both guardian_decision and denied signals so all lifecycle
792
793
  // observers are notified of the denial.
793
794
  const deniedPayload = {
794
- sourceChannel: approval.channel,
795
+ sourceChannel: approval.channel as NotificationSourceChannel,
795
796
  requesterExternalUserId: approval.requesterExternalUserId,
796
797
  requesterChatId: approval.requesterChatId,
797
798
  decidedByExternalUserId,
@@ -800,7 +801,7 @@ async function handleAccessRequestApproval(
800
801
 
801
802
  void emitNotificationSignal({
802
803
  sourceEventName: "ingress.trusted_contact.guardian_decision",
803
- sourceChannel: approval.channel,
804
+ sourceChannel: approval.channel as NotificationSourceChannel,
804
805
  sourceSessionId: approval.conversationId,
805
806
  attentionHints: {
806
807
  requiresAction: false,
@@ -814,7 +815,7 @@ async function handleAccessRequestApproval(
814
815
 
815
816
  void emitNotificationSignal({
816
817
  sourceEventName: "ingress.trusted_contact.denied",
817
- sourceChannel: approval.channel,
818
+ sourceChannel: approval.channel as NotificationSourceChannel,
818
819
  sourceSessionId: approval.conversationId,
819
820
  attentionHints: {
820
821
  requiresAction: false,
@@ -882,7 +883,7 @@ async function handleAccessRequestApproval(
882
883
  if (!decisionResult.verificationSessionId) {
883
884
  void emitNotificationSignal({
884
885
  sourceEventName: "ingress.trusted_contact.guardian_decision",
885
- sourceChannel: approval.channel,
886
+ sourceChannel: approval.channel as NotificationSourceChannel,
886
887
  sourceSessionId: approval.conversationId,
887
888
  attentionHints: {
888
889
  requiresAction: false,
@@ -891,7 +892,7 @@ async function handleAccessRequestApproval(
891
892
  visibleInSourceNow: false,
892
893
  },
893
894
  contextPayload: {
894
- sourceChannel: approval.channel,
895
+ sourceChannel: approval.channel as NotificationSourceChannel,
895
896
  requesterExternalUserId: approval.requesterExternalUserId,
896
897
  requesterChatId: approval.requesterChatId,
897
898
  decidedByExternalUserId,
@@ -908,7 +909,7 @@ async function handleAccessRequestApproval(
908
909
  if (decisionResult.verificationSessionId && codeDelivered) {
909
910
  void emitNotificationSignal({
910
911
  sourceEventName: "ingress.trusted_contact.verification_sent",
911
- sourceChannel: approval.channel,
912
+ sourceChannel: approval.channel as NotificationSourceChannel,
912
913
  sourceSessionId: approval.conversationId,
913
914
  attentionHints: {
914
915
  requiresAction: false,
@@ -917,7 +918,7 @@ async function handleAccessRequestApproval(
917
918
  visibleInSourceNow: true,
918
919
  },
919
920
  contextPayload: {
920
- sourceChannel: approval.channel,
921
+ sourceChannel: approval.channel as NotificationSourceChannel,
921
922
  requesterExternalUserId: approval.requesterExternalUserId,
922
923
  requesterChatId: approval.requesterChatId,
923
924
  verificationSessionId: decisionResult.verificationSessionId,
@@ -7,7 +7,10 @@
7
7
 
8
8
  import type { ChannelId } from "../../channels/types.js";
9
9
  import { getReadinessService } from "../../daemon/handlers/config-channels.js";
10
- import { getInviteAdapterRegistry } from "../channel-invite-transport.js";
10
+ import {
11
+ getInviteAdapterRegistry,
12
+ resolveAdapterHandle,
13
+ } from "../channel-invite-transport.js";
11
14
  import type { RouteDefinition } from "../http-router.js";
12
15
 
13
16
  /**
@@ -18,16 +21,20 @@ import type { RouteDefinition } from "../http-router.js";
18
21
  export async function handleGetChannelReadiness(url: URL): Promise<Response> {
19
22
  const channel =
20
23
  (url.searchParams.get("channel") as ChannelId | null) ?? undefined;
21
- const includeRemote = url.searchParams.get("includeRemote") === "true";
24
+ // Default to including remote checks — they're cached for 5 minutes and
25
+ // required for accurate readiness (e.g. email inbox existence).
26
+ const includeRemote = url.searchParams.get("includeRemote") !== "false";
22
27
 
23
28
  const service = getReadinessService();
24
29
  const snapshots = await service.getReadiness(channel, includeRemote);
25
30
  const adapterRegistry = getInviteAdapterRegistry();
26
31
 
27
- return Response.json({
28
- success: true,
29
- snapshots: snapshots.map((s) => {
32
+ const enriched = await Promise.all(
33
+ snapshots.map(async (s) => {
30
34
  const adapter = adapterRegistry.get(s.channel);
35
+ const channelHandle = adapter
36
+ ? await resolveAdapterHandle(adapter)
37
+ : undefined;
31
38
  return {
32
39
  channel: s.channel,
33
40
  ready: s.ready,
@@ -36,9 +43,14 @@ export async function handleGetChannelReadiness(url: URL): Promise<Response> {
36
43
  reasons: s.reasons,
37
44
  localChecks: s.localChecks,
38
45
  remoteChecks: s.remoteChecks,
39
- channelHandle: adapter?.resolveChannelHandle?.() ?? undefined,
46
+ channelHandle,
40
47
  };
41
48
  }),
49
+ );
50
+
51
+ return Response.json({
52
+ success: true,
53
+ snapshots: enriched,
42
54
  });
43
55
  }
44
56
 
@@ -66,14 +78,16 @@ export async function handleRefreshChannelReadiness(
66
78
 
67
79
  const snapshots = await service.getReadiness(
68
80
  body.channel,
69
- body.includeRemote,
81
+ body.includeRemote ?? true,
70
82
  );
71
83
  const adapterRegistry = getInviteAdapterRegistry();
72
84
 
73
- return Response.json({
74
- success: true,
75
- snapshots: snapshots.map((s) => {
85
+ const enriched = await Promise.all(
86
+ snapshots.map(async (s) => {
76
87
  const adapter = adapterRegistry.get(s.channel);
88
+ const channelHandle = adapter
89
+ ? await resolveAdapterHandle(adapter)
90
+ : undefined;
77
91
  return {
78
92
  channel: s.channel,
79
93
  ready: s.ready,
@@ -82,9 +96,14 @@ export async function handleRefreshChannelReadiness(
82
96
  reasons: s.reasons,
83
97
  localChecks: s.localChecks,
84
98
  remoteChecks: s.remoteChecks,
85
- channelHandle: adapter?.resolveChannelHandle?.() ?? undefined,
99
+ channelHandle,
86
100
  };
87
101
  }),
102
+ );
103
+
104
+ return Response.json({
105
+ success: true,
106
+ snapshots: enriched,
88
107
  });
89
108
  }
90
109
 
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Route handlers for contact management endpoints.
3
3
  *
4
- * GET /v1/contacts — list contacts
5
- * POST /v1/contacts — create or update a contact
6
- * GET /v1/contacts/:id — get a contact by ID
7
- * POST /v1/contacts/merge merge two contacts
8
- * PATCH /v1/contacts/channels/:id update a contact channel's status/policy
9
- * POST /v1/contacts/:contactId/channels/:channelId/verifyinitiate trusted contact verification
4
+ * GET /v1/contacts — list contacts
5
+ * POST /v1/contacts — create or update a contact
6
+ * GET /v1/contacts/:id — get a contact by ID
7
+ * DELETE /v1/contacts/:id delete a contact
8
+ * POST /v1/contacts/merge merge two contacts
9
+ * PATCH /v1/contact-channels/:contactChannelIdupdate a contact channel's status/policy
10
+ * POST /v1/contact-channels/:contactChannelId/verify — initiate trusted contact verification
10
11
  */
11
12
 
12
13
  import { createHash, randomBytes } from "node:crypto";
@@ -14,6 +15,7 @@ import { createHash, randomBytes } from "node:crypto";
14
15
  import type { ChannelId } from "../../channels/types.js";
15
16
  import { resolveGuardianName } from "../../config/user-reference.js";
16
17
  import {
18
+ deleteContact,
17
19
  getAssistantContactMetadata,
18
20
  getChannelById,
19
21
  getContact,
@@ -25,7 +27,6 @@ import {
25
27
  upsertContact,
26
28
  validateSpeciesMetadata,
27
29
  } from "../../contacts/contact-store.js";
28
- import type { ContactChannel } from "../../contacts/types.js";
29
30
  import type {
30
31
  AssistantSpecies,
31
32
  ChannelPolicy,
@@ -143,6 +144,20 @@ export function handleGetContact(contactId: string): Response {
143
144
  });
144
145
  }
145
146
 
147
+ /**
148
+ * DELETE /v1/contacts/:id
149
+ */
150
+ export function handleDeleteContact(contactId: string): Response {
151
+ const result = deleteContact(contactId);
152
+ if (result === "not_found") {
153
+ return httpError("NOT_FOUND", `Contact "${contactId}" not found`, 404);
154
+ }
155
+ if (result === "is_guardian") {
156
+ return httpError("FORBIDDEN", "Cannot delete a guardian contact", 403);
157
+ }
158
+ return new Response(null, { status: 204 });
159
+ }
160
+
146
161
  /**
147
162
  * POST /v1/contacts/merge { keepId, mergeId }
148
163
  */
@@ -339,7 +354,7 @@ export async function handleUpsertContact(req: Request): Promise<Response> {
339
354
  }
340
355
 
341
356
  /**
342
- * PATCH /v1/contacts/channels/:channelId { status?, policy?, reason? }
357
+ * PATCH /v1/contact-channels/:contactChannelId { status?, policy?, reason? }
343
358
  */
344
359
  export async function handleUpdateContactChannel(
345
360
  req: Request,
@@ -459,27 +474,32 @@ function getTelegramBotUsername(): string | undefined {
459
474
  }
460
475
 
461
476
  /**
462
- * POST /v1/contacts/:contactId/channels/:channelId/verify
477
+ * POST /v1/contact-channels/:contactChannelId/verify
463
478
  *
464
479
  * Initiate trusted contact verification for a specific channel. Sends a
465
480
  * verification code via SMS, Telegram, Slack, or voice and returns session
466
481
  * info so the client can track the verification flow.
467
482
  */
468
483
  export async function handleVerifyContactChannel(
469
- contactId: string,
470
- channelId: string,
484
+ contactChannelId: string,
471
485
  assistantId: string,
472
486
  ): Promise<Response> {
473
- const contact = getContact(contactId);
474
- if (!contact) {
475
- return httpError("NOT_FOUND", `Contact "${contactId}" not found`, 404);
487
+ const channel = getChannelById(contactChannelId);
488
+ if (!channel) {
489
+ return httpError(
490
+ "NOT_FOUND",
491
+ `Channel "${contactChannelId}" not found`,
492
+ 404,
493
+ );
476
494
  }
477
495
 
478
- const channel: ContactChannel | undefined = contact.channels.find(
479
- (ch) => ch.id === channelId,
480
- );
481
- if (!channel) {
482
- return httpError("NOT_FOUND", `Channel "${channelId}" not found`, 404);
496
+ const contact = getContact(channel.contactId);
497
+ if (!contact) {
498
+ return httpError(
499
+ "NOT_FOUND",
500
+ `Contact "${channel.contactId}" not found`,
501
+ 404,
502
+ );
483
503
  }
484
504
 
485
505
  // Already verified — no need to re-verify
@@ -605,6 +625,9 @@ export async function handleVerifyContactChannel(
605
625
  }
606
626
 
607
627
  // Telegram handle only (no chat ID): bootstrap flow
628
+ const { ensureTelegramBotUsernameResolved } =
629
+ await import("../channel-invite-transports/telegram.js");
630
+ await ensureTelegramBotUsernameResolved();
608
631
  const botUsername = getTelegramBotUsername();
609
632
  if (!botUsername) {
610
633
  return httpError(
@@ -713,20 +736,19 @@ export function contactRouteDefinitions(): RouteDefinition[] {
713
736
  handler: async ({ req }) => handleMergeContacts(req),
714
737
  },
715
738
  {
716
- endpoint: "contacts/channels/:id",
739
+ endpoint: "contact-channels/:contactChannelId",
717
740
  method: "PATCH",
718
- policyKey: "contacts/channels",
741
+ policyKey: "contact-channels",
719
742
  handler: async ({ req, params }) =>
720
- handleUpdateContactChannel(req, params.id),
743
+ handleUpdateContactChannel(req, params.contactChannelId),
721
744
  },
722
745
  {
723
- endpoint: "contacts/:contactId/channels/:channelId/verify",
746
+ endpoint: "contact-channels/:contactChannelId/verify",
724
747
  method: "POST",
725
- policyKey: "contacts/channels",
748
+ policyKey: "contact-channels",
726
749
  handler: async ({ params, authContext }) =>
727
750
  handleVerifyContactChannel(
728
- params.contactId,
729
- params.channelId,
751
+ params.contactChannelId,
730
752
  authContext.assistantId,
731
753
  ),
732
754
  },
@@ -746,5 +768,11 @@ export function contactCatchAllRouteDefinitions(): RouteDefinition[] {
746
768
  policyKey: "contacts",
747
769
  handler: ({ params }) => handleGetContact(params.id),
748
770
  },
771
+ {
772
+ endpoint: "contacts/:id",
773
+ method: "DELETE",
774
+ policyKey: "contacts",
775
+ handler: ({ params }) => handleDeleteContact(params.id),
776
+ },
749
777
  ];
750
778
  }
@@ -13,7 +13,6 @@
13
13
  */
14
14
  import type { ChannelId } from "../../../channels/types.js";
15
15
  import { getGatewayInternalBaseUrl } from "../../../config/env.js";
16
- import { RESEND_COOLDOWN_MS } from "../../../daemon/handlers/config-channels.js";
17
16
  import { getLogger } from "../../../util/logger.js";
18
17
  import { mintDaemonDeliveryToken } from "../../auth/token-service.js";
19
18
  import {
@@ -23,6 +22,7 @@ import {
23
22
  updateSessionDelivery,
24
23
  updateSessionStatus,
25
24
  } from "../../channel-guardian-service.js";
25
+ import { RESEND_COOLDOWN_MS } from "../../guardian-outbound-actions.js";
26
26
  import {
27
27
  composeVerificationTelegram,
28
28
  GUARDIAN_VERIFY_TEMPLATE_KEYS,
@@ -11,6 +11,7 @@ import type { ChannelId, InterfaceId } from "../../../channels/types.js";
11
11
  import { createCanonicalGuardianRequest } from "../../../memory/canonical-guardian-store.js";
12
12
  import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
13
13
  import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
14
+ import type { NotificationSourceChannel } from "../../../notifications/signal.js";
14
15
  import { getLogger } from "../../../util/logger.js";
15
16
  import { getGuardianBinding } from "../../channel-guardian-service.js";
16
17
  import { GUARDIAN_APPROVAL_TTL_MS } from "../channel-route-shared.js";
@@ -132,7 +133,7 @@ export function handleEscalationIntercept(
132
133
  // channels, supplementing the direct guardian notification below.
133
134
  void emitNotificationSignal({
134
135
  sourceEventName: "ingress.escalation",
135
- sourceChannel,
136
+ sourceChannel: sourceChannel as NotificationSourceChannel,
136
137
  sourceSessionId: conversationId,
137
138
  attentionHints: {
138
139
  requiresAction: true,
@@ -22,6 +22,7 @@ import {
22
22
  } from "../../../contacts/contacts-write.js";
23
23
  import * as channelDeliveryStore from "../../../memory/channel-delivery-store.js";
24
24
  import { emitNotificationSignal } from "../../../notifications/emit-signal.js";
25
+ import type { NotificationSourceChannel } from "../../../notifications/signal.js";
25
26
  import { canonicalizeInboundIdentity } from "../../../util/canonicalize-identity.js";
26
27
  import { getLogger } from "../../../util/logger.js";
27
28
  import {
@@ -230,7 +231,7 @@ export async function handleVerificationIntercept(
230
231
  if (verifyResult.verificationType === "trusted_contact") {
231
232
  void emitNotificationSignal({
232
233
  sourceEventName: "ingress.trusted_contact.activated",
233
- sourceChannel,
234
+ sourceChannel: sourceChannel as NotificationSourceChannel,
234
235
  sourceSessionId: conversationId,
235
236
  attentionHints: {
236
237
  requiresAction: false,
@@ -235,7 +235,7 @@ export async function handleStartOutbound(req: Request): Promise<Response> {
235
235
  );
236
236
  }
237
237
 
238
- const result = startOutbound({
238
+ const result = await startOutbound({
239
239
  channel: body.channel,
240
240
  destination: body.destination,
241
241
  rebind: body.rebind,