@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -23,47 +23,46 @@ export async function run(
23
23
  );
24
24
  }
25
25
 
26
- return withProviderToken(provider, async (token) => {
27
- const result = await provider.senderDigest!(token, query, {
28
- maxMessages,
29
- maxSenders,
30
- pageToken,
31
- });
32
-
33
- if (result.senders.length === 0) {
34
- return ok(
35
- JSON.stringify({
36
- senders: [],
37
- total_scanned: result.totalScanned,
38
- query_used: result.queryUsed,
39
- ...(result.truncated ? { truncated: true } : {}),
40
- message:
41
- "No emails found matching the query. Try broadening the search (e.g. remove category filter or extend date range).",
42
- }),
43
- );
44
- }
45
-
46
- // Map to snake_case output format for LLM consumption
47
- const senders = result.senders.map((s) => ({
48
- id: s.id,
49
- display_name: s.displayName,
50
- email: s.email,
51
- message_count: s.messageCount,
52
- has_unsubscribe: s.hasUnsubscribe,
53
- newest_message_id: s.newestMessageId,
54
- search_query: s.searchQuery,
55
- }));
26
+ const conn = getProviderConnection(provider);
27
+ const result = await provider.senderDigest!(conn, query, {
28
+ maxMessages,
29
+ maxSenders,
30
+ pageToken,
31
+ });
56
32
 
33
+ if (result.senders.length === 0) {
57
34
  return ok(
58
35
  JSON.stringify({
59
- senders,
36
+ senders: [],
60
37
  total_scanned: result.totalScanned,
61
38
  query_used: result.queryUsed,
62
39
  ...(result.truncated ? { truncated: true } : {}),
63
- note: `message_count reflects emails found per sender within the ${result.totalScanned} messages scanned. Use messaging_archive_by_sender with the sender's search_query to archive their messages.`,
40
+ message:
41
+ "No emails found matching the query. Try broadening the search (e.g. remove category filter or extend date range).",
64
42
  }),
65
43
  );
66
- });
44
+ }
45
+
46
+ // Map to snake_case output format for LLM consumption
47
+ const senders = result.senders.map((s) => ({
48
+ id: s.id,
49
+ display_name: s.displayName,
50
+ email: s.email,
51
+ message_count: s.messageCount,
52
+ has_unsubscribe: s.hasUnsubscribe,
53
+ newest_message_id: s.newestMessageId,
54
+ search_query: s.searchQuery,
55
+ }));
56
+
57
+ return ok(
58
+ JSON.stringify({
59
+ senders,
60
+ total_scanned: result.totalScanned,
61
+ query_used: result.queryUsed,
62
+ ...(result.truncated ? { truncated: true } : {}),
63
+ note: `message_count reflects emails found per sender within the ${result.totalScanned} messages scanned. Use messaging_archive_by_sender with the sender's search_query to archive their messages.`,
64
+ }),
65
+ );
67
66
  } catch (e) {
68
67
  return err(e instanceof Error ? e.message : String(e));
69
68
  }
@@ -8,7 +8,8 @@ import {
8
8
  getMessagingProvider,
9
9
  isPlatformEnabled,
10
10
  } from "../../../../messaging/registry.js";
11
- import { withValidToken } from "../../../../security/token-manager.js";
11
+ import type { OAuthConnection } from "../../../../oauth/connection.js";
12
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
12
13
  import type { ToolExecutionResult } from "../../../../tools/types.js";
13
14
 
14
15
  export function ok(content: string): ToolExecutionResult {
@@ -126,16 +127,15 @@ export function resolveProvider(platformInput?: string): MessagingProvider {
126
127
  }
127
128
 
128
129
  /**
129
- * Execute a callback with a valid OAuth token for the given provider.
130
- * Providers that manage their own auth (e.g. Telegram with a bot token)
131
- * expose isConnected() and don't need an OAuth access_token lookup.
130
+ * Resolve an OAuthConnection (or empty string for non-OAuth providers)
131
+ * for the given messaging provider.
132
+ *
133
+ * Non-OAuth providers (e.g. Telegram) use isConnected() and don't need
134
+ * tokens — they receive an empty string which the string overload handles.
132
135
  */
133
- export async function withProviderToken<T>(
136
+ export function getProviderConnection(
134
137
  provider: MessagingProvider,
135
- fn: (token: string) => Promise<T>,
136
- ): Promise<T> {
137
- if (provider.isConnected?.()) {
138
- return fn("");
139
- }
140
- return withValidToken(provider.credentialService, fn);
138
+ ): OAuthConnection | string {
139
+ if (provider.isConnected?.()) return "";
140
+ return resolveOAuthConnection(provider.credentialService);
141
141
  }
@@ -38,4 +38,4 @@ Thread grouping is handled by the LLM-powered decision engine, not by any parame
38
38
  ## Important
39
39
 
40
40
  - Do **NOT** use AppleScript `display notification` or other OS-level notification commands for assistant-managed alerts. Always use `send_notification`.
41
- - For sending rich content (digests, summaries, reports) to a specific chat, email, or SMS destination, use the messaging skill's `messaging_send` instead. The decision engine rewrites `send_notification` content into short alerts, which strips rich formatting.
41
+ - For sending rich content (digests, summaries, reports) to a specific chat or email destination, use the messaging skill's `messaging_send` instead. The decision engine rewrites `send_notification` content into short alerts, which strips rich formatting.
@@ -147,10 +147,10 @@ During an active call, the user can interact with the AI voice agent via the HTT
147
147
 
148
148
  #### Answering questions
149
149
 
150
- When the AI voice agent encounters something it needs user input for, it dispatches an **ASK_GUARDIAN** request to all configured guardian channels (mac desktop, Telegram, SMS). The call status changes to `waiting_on_user`.
150
+ When the AI voice agent encounters something it needs user input for, it dispatches an **ASK_GUARDIAN** request to all configured guardian channels (mac desktop, Telegram). The call status changes to `waiting_on_user`.
151
151
 
152
152
  1. The question is delivered simultaneously to every configured channel. The first channel to respond wins (first-response-wins semantics) -- once one channel provides an answer, the other channels receive a "already answered" notice.
153
- 2. On the mac desktop, a guardian request thread is created with the question. On Telegram/SMS, the question text and a request code are delivered via the gateway.
153
+ 2. On the mac desktop, a guardian request thread is created with the question. On Telegram, the question text and a request code are delivered via the gateway.
154
154
  3. If DTMF callee verification is enabled, the callee must enter a verification code before the call proceeds (see the **DTMF Callee Verification** section above).
155
155
  4. The guardian provides an answer through whichever channel they prefer. The answer is routed to the AI voice agent, which continues the conversation naturally.
156
156
 
@@ -160,16 +160,16 @@ When the AI voice agent encounters something it needs user input for, it dispatc
160
160
 
161
161
  When a consultation times out, the voice agent apologizes to the caller and moves on -- but the interaction is not lost. If the guardian responds after the timeout:
162
162
 
163
- 1. **Late reply detection**: The system recognizes the late answer on whichever channel it arrives (desktop, Telegram, or SMS) and presents a follow-up prompt asking the guardian what they would like to do.
163
+ 1. **Late reply detection**: The system recognizes the late answer on whichever channel it arrives (desktop or Telegram) and presents a follow-up prompt asking the guardian what they would like to do.
164
164
  2. **Follow-up options**: The guardian can choose to:
165
165
  - **Call back** the original caller with the answer
166
166
  - **Send a text message** to the caller with the answer
167
167
  - **Decline** if the follow-up is no longer needed
168
- 3. **Automatic execution**: If the guardian chooses to call back or send a message, the system resolves the original caller's phone number from the call record and executes the action automatically -- placing an outbound callback call or sending an SMS via the gateway.
168
+ 3. **Automatic execution**: If the guardian chooses to call back or send a message, the system resolves the original caller's phone number from the call record and executes the action automatically -- placing an outbound callback call or sending a message via the gateway.
169
169
 
170
170
  All user-facing messages in this flow (timeout acknowledgments, follow-up prompts, completion confirmations) are generated by the assistant to maintain a natural, conversational tone. No fixed/canned responses are used.
171
171
 
172
- The follow-up flow works across all guardian channels. The guardian can receive the timeout notice on Telegram, reply late via SMS, and choose to call back -- the system handles cross-channel routing transparently.
172
+ The follow-up flow works across all guardian channels. The guardian can receive the timeout notice on Telegram and choose to call back -- the system handles cross-channel routing transparently.
173
173
 
174
174
  #### Steering with instructions
175
175
 
@@ -147,7 +147,7 @@ Before confirming a schedule to the user, you MUST verify that you have the capa
147
147
  When `schedule_create` returns, it includes an integration status summary. Cross-reference the scheduled task's requirements against the available integrations:
148
148
 
149
149
  - If the task involves **email** (reading, sending, OTP verification): an email integration must be connected (check the "email" category)
150
- - If the task involves **sending SMS or making calls**: SMS/Twilio must be connected
150
+ - If the task involves **making calls**: Twilio must be connected
151
151
  - If the task involves **web browsing or form-filling**: browser automation must be available (check client type)
152
152
  - If the task involves a **multi-step workflow** (e.g., book appointment → read confirmation email), trace the full dependency chain
153
153
 
@@ -2,8 +2,8 @@
2
2
  name: skill-management
3
3
  description: Create and delete custom managed skills
4
4
  metadata:
5
+ emoji: "\U0001F9E9"
5
6
  vellum:
6
- emoji: "\U0001F9E9"
7
7
  display-name: "Skill Management"
8
8
  user-invocable: true
9
9
  ---
@@ -2,8 +2,8 @@
2
2
  * Shared utilities for slack skill tools.
3
3
  */
4
4
 
5
- import { getMessagingProvider } from "../../../../messaging/registry.js";
6
- import { withValidToken } from "../../../../security/token-manager.js";
5
+ import type { OAuthConnection } from "../../../../oauth/connection.js";
6
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
7
7
  import type { ToolExecutionResult } from "../../../../tools/types.js";
8
8
 
9
9
  export function ok(content: string): ToolExecutionResult {
@@ -14,12 +14,6 @@ export function err(message: string): ToolExecutionResult {
14
14
  return { content: message, isError: true };
15
15
  }
16
16
 
17
- /**
18
- * Execute a callback with a valid Slack OAuth token.
19
- */
20
- export async function withSlackToken<T>(
21
- fn: (token: string) => Promise<T>,
22
- ): Promise<T> {
23
- const provider = getMessagingProvider("slack");
24
- return withValidToken(provider.credentialService, fn);
17
+ export function getSlackConnection(): OAuthConnection {
18
+ return resolveOAuthConnection("integration:slack");
25
19
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ToolContext,
4
4
  ToolExecutionResult,
5
5
  } from "../../../../tools/types.js";
6
- import { err, ok, withSlackToken } from "./shared.js";
6
+ import { err, getSlackConnection, ok } from "./shared.js";
7
7
 
8
8
  export async function run(
9
9
  input: Record<string, unknown>,
@@ -18,10 +18,9 @@ export async function run(
18
18
  }
19
19
 
20
20
  try {
21
- return await withSlackToken(async (token) => {
22
- await addReaction(token, channel, timestamp, emoji);
23
- return ok(`Added :${emoji}: reaction.`);
24
- });
21
+ const connection = getSlackConnection();
22
+ await addReaction(connection, channel, timestamp, emoji);
23
+ return ok(`Added :${emoji}: reaction.`);
25
24
  } catch (e) {
26
25
  return err(e instanceof Error ? e.message : String(e));
27
26
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ToolContext,
4
4
  ToolExecutionResult,
5
5
  } from "../../../../tools/types.js";
6
- import { err, ok, withSlackToken } from "./shared.js";
6
+ import { err, getSlackConnection, ok } from "./shared.js";
7
7
 
8
8
  export async function run(
9
9
  input: Record<string, unknown>,
@@ -16,23 +16,22 @@ export async function run(
16
16
  }
17
17
 
18
18
  try {
19
- return await withSlackToken(async (token) => {
20
- const resp = await slack.conversationInfo(token, channelId);
21
- const conv = resp.channel;
19
+ const connection = getSlackConnection();
20
+ const resp = await slack.conversationInfo(connection, channelId);
21
+ const conv = resp.channel;
22
22
 
23
- const result = {
24
- channelId: conv.id,
25
- name: conv.name ?? conv.id,
26
- topic: conv.topic?.value || null,
27
- purpose: conv.purpose?.value || null,
28
- isPrivate: conv.is_private ?? conv.is_group ?? false,
29
- isArchived: conv.is_archived ?? false,
30
- memberCount: conv.num_members ?? null,
31
- latestActivityTs: conv.latest?.ts ?? null,
32
- };
23
+ const result = {
24
+ channelId: conv.id,
25
+ name: conv.name ?? conv.id,
26
+ topic: conv.topic?.value || null,
27
+ purpose: conv.purpose?.value || null,
28
+ isPrivate: conv.is_private ?? conv.is_group ?? false,
29
+ isArchived: conv.is_archived ?? false,
30
+ memberCount: conv.num_members ?? null,
31
+ latestActivityTs: conv.latest?.ts ?? null,
32
+ };
33
33
 
34
- return ok(JSON.stringify(result, null, 2));
35
- });
34
+ return ok(JSON.stringify(result, null, 2));
36
35
  } catch (e) {
37
36
  return err(e instanceof Error ? e.message : String(e));
38
37
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ToolContext,
4
4
  ToolExecutionResult,
5
5
  } from "../../../../tools/types.js";
6
- import { err, ok, withSlackToken } from "./shared.js";
6
+ import { err, getSlackConnection, ok } from "./shared.js";
7
7
 
8
8
  export async function run(
9
9
  input: Record<string, unknown>,
@@ -17,10 +17,9 @@ export async function run(
17
17
  }
18
18
 
19
19
  try {
20
- return await withSlackToken(async (token) => {
21
- await deleteMessage(token, channel, timestamp);
22
- return ok(`Message deleted.`);
23
- });
20
+ const connection = getSlackConnection();
21
+ await deleteMessage(connection, channel, timestamp);
22
+ return ok(`Message deleted.`);
24
23
  } catch (e) {
25
24
  return err(e instanceof Error ? e.message : String(e));
26
25
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ToolContext,
4
4
  ToolExecutionResult,
5
5
  } from "../../../../tools/types.js";
6
- import { err, ok, withSlackToken } from "./shared.js";
6
+ import { err, getSlackConnection, ok } from "./shared.js";
7
7
 
8
8
  export async function run(
9
9
  input: Record<string, unknown>,
@@ -18,10 +18,9 @@ export async function run(
18
18
  }
19
19
 
20
20
  try {
21
- return await withSlackToken(async (token) => {
22
- await updateMessage(token, channel, timestamp, text);
23
- return ok(`Message updated.`);
24
- });
21
+ const connection = getSlackConnection();
22
+ await updateMessage(connection, channel, timestamp, text);
23
+ return ok(`Message updated.`);
25
24
  } catch (e) {
26
25
  return err(e instanceof Error ? e.message : String(e));
27
26
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ToolContext,
4
4
  ToolExecutionResult,
5
5
  } from "../../../../tools/types.js";
6
- import { err, ok, withSlackToken } from "./shared.js";
6
+ import { err, getSlackConnection, ok } from "./shared.js";
7
7
 
8
8
  export async function run(
9
9
  input: Record<string, unknown>,
@@ -16,10 +16,9 @@ export async function run(
16
16
  }
17
17
 
18
18
  try {
19
- return await withSlackToken(async (token) => {
20
- await leaveConversation(token, channel);
21
- return ok("Left channel.");
22
- });
19
+ const connection = getSlackConnection();
20
+ await leaveConversation(connection, channel);
21
+ return ok("Left channel.");
23
22
  } catch (e) {
24
23
  return err(e instanceof Error ? e.message : String(e));
25
24
  }
@@ -1,11 +1,12 @@
1
1
  import { getConfig } from "../../../../config/loader.js";
2
2
  import * as slack from "../../../../messaging/providers/slack/client.js";
3
3
  import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
4
+ import type { OAuthConnection } from "../../../../oauth/connection.js";
4
5
  import type {
5
6
  ToolContext,
6
7
  ToolExecutionResult,
7
8
  } from "../../../../tools/types.js";
8
- import { err, ok, withSlackToken } from "./shared.js";
9
+ import { err, getSlackConnection, ok } from "./shared.js";
9
10
 
10
11
  interface ThreadSummary {
11
12
  threadTs: string;
@@ -26,13 +27,16 @@ interface ChannelDigest {
26
27
 
27
28
  const userNameCache = new Map<string, string>();
28
29
 
29
- async function resolveUserName(token: string, userId: string): Promise<string> {
30
+ async function resolveUserName(
31
+ connection: OAuthConnection,
32
+ userId: string,
33
+ ): Promise<string> {
30
34
  if (!userId) return "unknown";
31
35
  const cached = userNameCache.get(userId);
32
36
  if (cached) return cached;
33
37
 
34
38
  try {
35
- const resp = await slack.userInfo(token, userId);
39
+ const resp = await slack.userInfo(connection, userId);
36
40
  const name =
37
41
  resp.user.profile?.display_name ||
38
42
  resp.user.profile?.real_name ||
@@ -46,7 +50,7 @@ async function resolveUserName(token: string, userId: string): Promise<string> {
46
50
  }
47
51
 
48
52
  async function scanChannel(
49
- token: string,
53
+ connection: OAuthConnection,
50
54
  conv: SlackConversation,
51
55
  oldestTs: string,
52
56
  includeThreads: boolean,
@@ -57,7 +61,7 @@ async function scanChannel(
57
61
 
58
62
  try {
59
63
  const history = await slack.conversationHistory(
60
- token,
64
+ connection,
61
65
  channelId,
62
66
  100,
63
67
  undefined,
@@ -71,7 +75,7 @@ async function scanChannel(
71
75
  }
72
76
 
73
77
  const keyParticipants = await Promise.all(
74
- [...participantIds].map((uid) => resolveUserName(token, uid)),
78
+ [...participantIds].map((uid) => resolveUserName(connection, uid)),
75
79
  );
76
80
 
77
81
  const threadMessages = messages
@@ -86,7 +90,7 @@ async function scanChannel(
86
90
  if (includeThreads) {
87
91
  try {
88
92
  const replies = await slack.conversationReplies(
89
- token,
93
+ connection,
90
94
  channelId,
91
95
  msg.ts,
92
96
  10,
@@ -97,11 +101,11 @@ async function scanChannel(
97
101
  }
98
102
  participants = await Promise.all(
99
103
  [...threadParticipantIds].map((uid) =>
100
- resolveUserName(token, uid),
104
+ resolveUserName(connection, uid),
101
105
  ),
102
106
  );
103
107
  } catch {
104
- participants = [await resolveUserName(token, msg.user ?? "")];
108
+ participants = [await resolveUserName(connection, msg.user ?? "")];
105
109
  }
106
110
  }
107
111
 
@@ -260,15 +264,34 @@ export async function run(
260
264
  const format = (input.format as string) ?? "text";
261
265
 
262
266
  try {
263
- return await withSlackToken(async (token) => {
264
- const oldestTs = String((Date.now() - hoursBack * 60 * 60 * 1000) / 1000);
267
+ const connection = getSlackConnection();
268
+ const oldestTs = String((Date.now() - hoursBack * 60 * 60 * 1000) / 1000);
265
269
 
266
- let channelsToScan: SlackConversation[];
267
- let failedLookups = 0;
270
+ let channelsToScan: SlackConversation[];
271
+ let failedLookups = 0;
268
272
 
269
- if (channelIds?.length) {
273
+ if (channelIds?.length) {
274
+ const results = await Promise.allSettled(
275
+ channelIds.map((id) => slack.conversationInfo(connection, id)),
276
+ );
277
+ channelsToScan = results
278
+ .filter(
279
+ (
280
+ r,
281
+ ): r is PromiseFulfilledResult<
282
+ Awaited<ReturnType<typeof slack.conversationInfo>>
283
+ > => r.status === "fulfilled",
284
+ )
285
+ .map((r) => r.value.channel);
286
+ failedLookups = results.filter((r) => r.status === "rejected").length;
287
+ } else {
288
+ const config = getConfig();
289
+ const preferredIds = config.skills?.entries?.slack?.config
290
+ ?.preferredChannels as string[] | undefined;
291
+
292
+ if (preferredIds?.length) {
270
293
  const results = await Promise.allSettled(
271
- channelIds.map((id) => slack.conversationInfo(token, id)),
294
+ preferredIds.map((id) => slack.conversationInfo(connection, id)),
272
295
  );
273
296
  channelsToScan = results
274
297
  .filter(
@@ -281,89 +304,69 @@ export async function run(
281
304
  .map((r) => r.value.channel);
282
305
  failedLookups = results.filter((r) => r.status === "rejected").length;
283
306
  } else {
284
- const config = getConfig();
285
- const preferredIds = config.skills?.entries?.slack?.config
286
- ?.preferredChannels as string[] | undefined;
287
-
288
- if (preferredIds?.length) {
289
- const results = await Promise.allSettled(
290
- preferredIds.map((id) => slack.conversationInfo(token, id)),
307
+ const allChannels: SlackConversation[] = [];
308
+ let cursor: string | undefined;
309
+ do {
310
+ const resp = await slack.listConversations(
311
+ connection,
312
+ "public_channel,private_channel",
313
+ true,
314
+ 200,
315
+ cursor,
291
316
  );
292
- channelsToScan = results
293
- .filter(
294
- (
295
- r,
296
- ): r is PromiseFulfilledResult<
297
- Awaited<ReturnType<typeof slack.conversationInfo>>
298
- > => r.status === "fulfilled",
299
- )
300
- .map((r) => r.value.channel);
301
- failedLookups = results.filter((r) => r.status === "rejected").length;
302
- } else {
303
- const allChannels: SlackConversation[] = [];
304
- let cursor: string | undefined;
305
- do {
306
- const resp = await slack.listConversations(
307
- token,
308
- "public_channel,private_channel",
309
- true,
310
- 200,
311
- cursor,
312
- );
313
- allChannels.push(...resp.channels);
314
- cursor = resp.response_metadata?.next_cursor || undefined;
315
- } while (cursor);
316
-
317
- channelsToScan = allChannels
318
- .filter((c) => c.is_member)
319
- .sort((a, b) => {
320
- const aTs = a.latest?.ts ? parseFloat(a.latest.ts) : 0;
321
- const bTs = b.latest?.ts ? parseFloat(b.latest.ts) : 0;
322
- return bTs - aTs;
323
- })
324
- .slice(0, maxChannels);
325
- }
317
+ allChannels.push(...resp.channels);
318
+ cursor = resp.response_metadata?.next_cursor || undefined;
319
+ } while (cursor);
320
+
321
+ channelsToScan = allChannels
322
+ .filter((c) => c.is_member)
323
+ .sort((a, b) => {
324
+ const aTs = a.latest?.ts ? parseFloat(a.latest.ts) : 0;
325
+ const bTs = b.latest?.ts ? parseFloat(b.latest.ts) : 0;
326
+ return bTs - aTs;
327
+ })
328
+ .slice(0, maxChannels);
326
329
  }
330
+ }
327
331
 
328
- const scanResults = await Promise.allSettled(
329
- channelsToScan.map((conv) =>
330
- scanChannel(token, conv, oldestTs, includeThreads),
331
- ),
332
- );
333
-
334
- const digests: ChannelDigest[] = scanResults
335
- .filter(
336
- (r): r is PromiseFulfilledResult<ChannelDigest> =>
337
- r.status === "fulfilled",
338
- )
339
- .map((r) => r.value)
340
- .filter((d) => d.messageCount > 0 || d.error);
341
-
342
- const skippedCount = scanResults.filter(
343
- (r) => r.status === "rejected",
344
- ).length;
345
-
346
- if (format === "blocks") {
347
- const blocks = buildBlockKitOutput(
348
- digests,
349
- hoursBack,
350
- channelsToScan.length,
351
- skippedCount,
352
- );
353
- return ok(JSON.stringify({ blocks }, null, 2));
354
- }
332
+ const scanResults = await Promise.allSettled(
333
+ channelsToScan.map((conv) =>
334
+ scanChannel(connection, conv, oldestTs, includeThreads),
335
+ ),
336
+ );
355
337
 
356
- const result = {
357
- scannedChannels: digests.length,
358
- totalChannelsAttempted: channelsToScan.length,
359
- skippedDueToErrors: skippedCount,
360
- failedLookups,
338
+ const digests: ChannelDigest[] = scanResults
339
+ .filter(
340
+ (r): r is PromiseFulfilledResult<ChannelDigest> =>
341
+ r.status === "fulfilled",
342
+ )
343
+ .map((r) => r.value)
344
+ .filter((d) => d.messageCount > 0 || d.error);
345
+
346
+ const skippedCount = scanResults.filter(
347
+ (r) => r.status === "rejected",
348
+ ).length;
349
+
350
+ if (format === "blocks") {
351
+ const blocks = buildBlockKitOutput(
352
+ digests,
361
353
  hoursBack,
362
- channels: digests,
363
- };
354
+ channelsToScan.length,
355
+ skippedCount,
356
+ );
357
+ return ok(JSON.stringify({ blocks }, null, 2));
358
+ }
364
359
 
365
- return ok(JSON.stringify(result, null, 2));
366
- });
360
+ const result = {
361
+ scannedChannels: digests.length,
362
+ totalChannelsAttempted: channelsToScan.length,
363
+ skippedDueToErrors: skippedCount,
364
+ failedLookups,
365
+ hoursBack,
366
+ channels: digests,
367
+ };
368
+
369
+ return ok(JSON.stringify(result, null, 2));
367
370
  } catch (e) {
368
371
  return err(e instanceof Error ? e.message : String(e));
369
372
  }
@@ -333,6 +333,12 @@ export function loadConfig(): AssistantConfig {
333
333
  }
334
334
 
335
335
  // Environment variables override everything
336
+ if (process.env.VELLUM_CONFIG_SANDBOX_ENABLED === "false") {
337
+ config.sandbox.enabled = false;
338
+ } else if (process.env.VELLUM_CONFIG_SANDBOX_ENABLED === "true") {
339
+ config.sandbox.enabled = true;
340
+ }
341
+
336
342
  if (process.env.ANTHROPIC_API_KEY) {
337
343
  config.apiKeys.anthropic = process.env.ANTHROPIC_API_KEY;
338
344
  }