@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Tool for managing per-channel permission profiles in Slack.
3
+ *
4
+ * Allows the assistant to configure which tools are available in specific
5
+ * Slack channels, set trust levels, and manage permission overrides.
6
+ */
7
+
8
+ import {
9
+ type ChannelPermissionProfile,
10
+ getChannelPermissions,
11
+ removeChannelPermissionProfile,
12
+ setAllChannelPermissions,
13
+ setChannelPermissionProfile,
14
+ } from "../../../../config/channel-permission-profiles.js";
15
+ import type {
16
+ ToolContext,
17
+ ToolExecutionResult,
18
+ } from "../../../../tools/types.js";
19
+ import { err, ok } from "./shared.js";
20
+
21
+ export async function run(
22
+ input: Record<string, unknown>,
23
+ _context: ToolContext,
24
+ ): Promise<ToolExecutionResult> {
25
+ const action = input.action as string;
26
+
27
+ if (!action) {
28
+ return err("action is required (list, get, set, remove, or clear).");
29
+ }
30
+
31
+ try {
32
+ switch (action) {
33
+ case "list": {
34
+ const perms = getChannelPermissions();
35
+ const entries = Object.entries(perms);
36
+ if (entries.length === 0) {
37
+ return ok(
38
+ 'No channel permission profiles configured. Use action "set" to configure permissions for a channel.',
39
+ );
40
+ }
41
+ return ok(JSON.stringify({ channelPermissions: perms }, null, 2));
42
+ }
43
+
44
+ case "get": {
45
+ const channelId = input.channel_id as string | undefined;
46
+ if (!channelId) {
47
+ return err('channel_id is required for "get" action.');
48
+ }
49
+ const perms = getChannelPermissions();
50
+ const profile = perms[channelId];
51
+ if (!profile) {
52
+ return ok(
53
+ JSON.stringify(
54
+ {
55
+ channel_id: channelId,
56
+ profile: null,
57
+ message: "No permission profile configured for this channel.",
58
+ },
59
+ null,
60
+ 2,
61
+ ),
62
+ );
63
+ }
64
+ return ok(JSON.stringify({ channel_id: channelId, profile }, null, 2));
65
+ }
66
+
67
+ case "set": {
68
+ const channelId = input.channel_id as string | undefined;
69
+ if (!channelId) {
70
+ return err('channel_id is required for "set" action.');
71
+ }
72
+
73
+ const profile: ChannelPermissionProfile = {};
74
+
75
+ if (typeof input.label === "string") {
76
+ profile.label = input.label;
77
+ }
78
+ if (Array.isArray(input.allowed_tool_categories)) {
79
+ profile.allowedToolCategories =
80
+ input.allowed_tool_categories as string[];
81
+ }
82
+ if (Array.isArray(input.blocked_tools)) {
83
+ profile.blockedTools = input.blocked_tools as string[];
84
+ }
85
+ if (
86
+ input.trust_level === "restricted" ||
87
+ input.trust_level === "standard"
88
+ ) {
89
+ profile.trustLevel = input.trust_level;
90
+ }
91
+
92
+ setChannelPermissionProfile(channelId, profile);
93
+ return ok(
94
+ JSON.stringify(
95
+ {
96
+ channel_id: channelId,
97
+ profile,
98
+ message: "Permission profile saved.",
99
+ },
100
+ null,
101
+ 2,
102
+ ),
103
+ );
104
+ }
105
+
106
+ case "remove": {
107
+ const channelId = input.channel_id as string | undefined;
108
+ if (!channelId) {
109
+ return err('channel_id is required for "remove" action.');
110
+ }
111
+ const removed = removeChannelPermissionProfile(channelId);
112
+ return ok(
113
+ JSON.stringify(
114
+ {
115
+ channel_id: channelId,
116
+ removed,
117
+ message: removed
118
+ ? "Permission profile removed."
119
+ : "No permission profile found for this channel.",
120
+ },
121
+ null,
122
+ 2,
123
+ ),
124
+ );
125
+ }
126
+
127
+ case "clear": {
128
+ setAllChannelPermissions({});
129
+ return ok(
130
+ JSON.stringify(
131
+ { message: "All channel permission profiles cleared." },
132
+ null,
133
+ 2,
134
+ ),
135
+ );
136
+ }
137
+
138
+ default:
139
+ return err(
140
+ `Unknown action "${action}". Use list, get, set, remove, or clear.`,
141
+ );
142
+ }
143
+ } catch (e) {
144
+ return err(e instanceof Error ? e.message : String(e));
145
+ }
146
+ }
@@ -140,6 +140,115 @@ function truncate(text: string, maxLen: number): string {
140
140
  return text.slice(0, maxLen - 3) + "...";
141
141
  }
142
142
 
143
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
+ type BlockKitBlock = Record<string, any>;
145
+
146
+ /**
147
+ * Build Slack Block Kit blocks from digest results.
148
+ * Produces a structured Block Kit payload with header, per-channel sections,
149
+ * thread context blocks, and dividers.
150
+ */
151
+ function buildBlockKitOutput(
152
+ digests: ChannelDigest[],
153
+ hoursBack: number,
154
+ totalAttempted: number,
155
+ skippedCount: number,
156
+ ): BlockKitBlock[] {
157
+ const blocks: BlockKitBlock[] = [];
158
+
159
+ // Header block with scan summary
160
+ blocks.push({
161
+ type: "header",
162
+ text: {
163
+ type: "plain_text",
164
+ text: `Slack Digest — ${digests.length} channel${digests.length !== 1 ? "s" : ""} scanned`,
165
+ },
166
+ });
167
+
168
+ blocks.push({
169
+ type: "section",
170
+ text: {
171
+ type: "mrkdwn",
172
+ text: `*Time range:* Last ${hoursBack} hour${hoursBack !== 1 ? "s" : ""} | *Channels attempted:* ${totalAttempted} | *Skipped:* ${skippedCount}`,
173
+ },
174
+ });
175
+
176
+ blocks.push({ type: "divider" });
177
+
178
+ for (const digest of digests) {
179
+ if (digest.error) {
180
+ blocks.push({
181
+ type: "section",
182
+ text: {
183
+ type: "mrkdwn",
184
+ text: `${digest.isPrivate ? "\ud83d\udd12 " : ""}*#${digest.channelName}* — _Error: ${digest.error}_`,
185
+ },
186
+ });
187
+ blocks.push({ type: "divider" });
188
+ continue;
189
+ }
190
+
191
+ // Channel section with name, message count, privacy indicator
192
+ const privacyIcon = digest.isPrivate ? "\ud83d\udd12 " : "";
193
+ blocks.push({
194
+ type: "section",
195
+ text: {
196
+ type: "mrkdwn",
197
+ text: `${privacyIcon}*#${digest.channelName}* — ${digest.messageCount} message${digest.messageCount !== 1 ? "s" : ""}`,
198
+ },
199
+ });
200
+
201
+ // Key participants as context
202
+ if (digest.keyParticipants.length > 0) {
203
+ blocks.push({
204
+ type: "context",
205
+ elements: [
206
+ {
207
+ type: "mrkdwn",
208
+ text: `*Active:* ${digest.keyParticipants.join(", ")}`,
209
+ },
210
+ ],
211
+ });
212
+ }
213
+
214
+ // Thread previews as context blocks
215
+ for (const thread of digest.topThreads) {
216
+ const participantText =
217
+ thread.participants.length > 0
218
+ ? thread.participants.join(", ")
219
+ : "unknown";
220
+ blocks.push({
221
+ type: "context",
222
+ elements: [
223
+ {
224
+ type: "mrkdwn",
225
+ text: `\ud83e\uddf5 *${thread.replyCount} replies* (${participantText}): ${thread.previewText}`,
226
+ },
227
+ ],
228
+ });
229
+ }
230
+
231
+ blocks.push({ type: "divider" });
232
+ }
233
+
234
+ // Slack Block Kit enforces a 50-block maximum per message.
235
+ // Truncate and append a summary block when we exceed the limit.
236
+ const SLACK_BLOCK_LIMIT = 50;
237
+ if (blocks.length > SLACK_BLOCK_LIMIT) {
238
+ const overflow = blocks.length - (SLACK_BLOCK_LIMIT - 1);
239
+ blocks.length = SLACK_BLOCK_LIMIT - 1;
240
+ blocks.push({
241
+ type: "section",
242
+ text: {
243
+ type: "mrkdwn",
244
+ text: `_... and ${overflow} more block${overflow !== 1 ? "s" : ""} truncated (some channels omitted). Use \`channel_ids\` to drill into specific channels._`,
245
+ },
246
+ });
247
+ }
248
+
249
+ return blocks;
250
+ }
251
+
143
252
  export async function run(
144
253
  input: Record<string, unknown>,
145
254
  _context: ToolContext,
@@ -148,6 +257,7 @@ export async function run(
148
257
  const hoursBack = (input.hours_back as number) ?? 24;
149
258
  const includeThreads = (input.include_threads as boolean) ?? true;
150
259
  const maxChannels = (input.max_channels as number) ?? 20;
260
+ const format = (input.format as string) ?? "text";
151
261
 
152
262
  try {
153
263
  return await withSlackToken(async (token) => {
@@ -233,6 +343,16 @@ export async function run(
233
343
  (r) => r.status === "rejected",
234
344
  ).length;
235
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
+ }
355
+
236
356
  const result = {
237
357
  scannedChannels: digests.length,
238
358
  totalChannelsAttempted: channelsToScan.length,
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: "Slack App Setup"
3
+ description: "Connect a Slack app to the Vellum Assistant via Socket Mode with guided app creation and guardian verification"
4
+ user-invocable: true
5
+ includes: ["guardian-verify-setup"]
6
+ metadata: { "vellum": { "emoji": "💬" } }
7
+ ---
8
+
9
+ You are helping your user connect a Slack bot to the Vellum Assistant gateway via Socket Mode. The gateway manages the Socket Mode connection — it never hits the assistant runtime directly. When this skill is invoked, walk through each step below using only existing tools.
10
+
11
+ ## Prerequisites — Check Before Starting
12
+
13
+ Before beginning setup, verify these conditions are met:
14
+
15
+ 1. **Gateway API base URL is set and reachable:** Use the injected `INTERNAL_GATEWAY_BASE_URL`, then run `curl -sf "$INTERNAL_GATEWAY_BASE_URL/healthz"` — it should return gateway health JSON (for example `{"status":"ok"}`). If it fails, tell the user to start the assistant with `vellum wake` and wait for it to become healthy before continuing.
16
+ 2. **Use gateway control-plane routes only.** Slack setup/config actions in this skill must call gateway endpoints under `/v1/integrations/slack/channel/*` — never call the assistant runtime port directly.
17
+
18
+ ## Setup Steps
19
+
20
+ ### Step 1: Generate Manifest & Create Slack App
21
+
22
+ Ask the user what they'd like to name their Slack bot and optionally provide a short description. Use their answers (or sensible defaults) to generate a pre-configured Slack app manifest.
23
+
24
+ Generate the manifest JSON:
25
+
26
+ ```json
27
+ {
28
+ "display_information": {
29
+ "name": "<user's chosen name>",
30
+ "description": "<user's chosen description>",
31
+ "background_color": "#1a1a2e"
32
+ },
33
+ "features": {
34
+ "app_home": {
35
+ "home_tab_enabled": true,
36
+ "messages_tab_enabled": true,
37
+ "messages_tab_read_only_enabled": false
38
+ },
39
+ "bot_user": {
40
+ "display_name": "<user's chosen name>",
41
+ "always_online": true
42
+ }
43
+ },
44
+ "oauth_config": {
45
+ "scopes": {
46
+ "bot": [
47
+ "app_mentions:read",
48
+ "channels:history",
49
+ "chat:write",
50
+ "files:write",
51
+ "im:history",
52
+ "im:read",
53
+ "im:write",
54
+ "reactions:write",
55
+ "users:read"
56
+ ]
57
+ }
58
+ },
59
+ "settings": {
60
+ "event_subscriptions": {
61
+ "bot_events": [
62
+ "app_home_opened",
63
+ "app_mention",
64
+ "message.channels",
65
+ "message.im"
66
+ ]
67
+ },
68
+ "interactivity": { "is_enabled": true },
69
+ "org_deploy_enabled": false,
70
+ "socket_mode_enabled": true,
71
+ "token_rotation_enabled": false
72
+ }
73
+ }
74
+ ```
75
+
76
+ After generating the manifest, URL-encode it and construct the create-from-manifest link:
77
+
78
+ ```
79
+ https://api.slack.com/apps?new_app=1&manifest_json=<url_encoded_manifest>
80
+ ```
81
+
82
+ Present the link to the user: "Click this link to create your Slack app. It's pre-configured with all the right permissions, events, and Socket Mode. Just select your workspace and click **Create**."
83
+
84
+ The manifest pre-configures everything needed: Socket Mode, 9 minimal bot scopes, 4 event subscriptions, bot user, App Home, and interactivity. No manual scope or event configuration is needed.
85
+
86
+ Wait for the user to confirm they've created the app before proceeding.
87
+
88
+ ### Step 2: Generate App Token & Collect It
89
+
90
+ Tell the user to navigate to **Settings > Basic Information > App-Level Tokens** in their newly created Slack app, then:
91
+
92
+ 1. Click **Generate Token and Scopes**
93
+ 2. Token name: "Socket Mode" (or any name they prefer)
94
+ 3. Add scope: `connections:write`
95
+ 4. Click **Generate**
96
+
97
+ **Immediately** collect the app token securely:
98
+
99
+ - Call `credential_store` with `action: "prompt"`, `service: "slack_channel"`, `field: "app_token"`, `label: "App-Level Token"`, `placeholder: "xapp-..."`, `description: "Paste the App-Level Token you just generated"`
100
+
101
+ **IMPORTANT — Secure credential collection only:** Never accept tokens pasted in plaintext chat. If the user pastes a token in the conversation, inform them that for security reasons you cannot use tokens shared in chat and must collect it through the secure prompt instead.
102
+
103
+ ### Step 3: Install App & Collect Bot Token
104
+
105
+ Tell the user to navigate to **Settings > Install App** in the sidebar, then click **Install to Workspace** and authorize the requested permissions (already pre-configured from the manifest).
106
+
107
+ After installation, collect the bot token securely:
108
+
109
+ - Call `credential_store` with `action: "prompt"`, `service: "slack_channel"`, `field: "bot_token"`, `label: "Bot User OAuth Token"`, `placeholder: "xoxb-..."`, `description: "Paste the Bot User OAuth Token shown after installing"`
110
+
111
+ **IMPORTANT — Secure credential collection only:** Never accept tokens pasted in plaintext chat. Always collect through the secure prompt.
112
+
113
+ ### Step 4: Validate & Connect
114
+
115
+ After both tokens are collected, submit them to the gateway for validation and storage:
116
+
117
+ ```bash
118
+ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/slack/channel/config" \
119
+ -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
120
+ -H "Content-Type: application/json" \
121
+ -d '{"botToken": "<bot_token>", "appToken": "<app_token>"}'
122
+ ```
123
+
124
+ The endpoint validates the bot token via Slack's `auth.test` API and stores both tokens in the secure key store.
125
+
126
+ **On success** (`success: true`, `connected: true`):
127
+
128
+ - Report the bot username and workspace name to the user
129
+ - Proceed to Step 5
130
+
131
+ **On failure:**
132
+
133
+ | Error | Action |
134
+ | ------------------------ | ------------------------------------------------------------------------------------ |
135
+ | `invalid_auth` | Bot token is invalid — re-prompt for the correct token (repeat Step 3 collection) |
136
+ | Missing scopes | Tell the user which scopes to add in the Slack app configuration |
137
+ | Socket Mode not enabled | Instruct the user to enable Socket Mode in the app settings |
138
+ | App token format invalid | Must start with `xapp-` — re-prompt for the correct token (repeat Step 2 collection) |
139
+
140
+ ### Step 5: Guardian Verification
141
+
142
+ Tell the user: "Now let's verify your identity as the trusted guardian for Slack."
143
+
144
+ Load the **guardian-verify-setup** skill to handle the verification flow:
145
+
146
+ - Call `skill_load` with `skill: "guardian-verify-setup"` to load the dependency skill.
147
+
148
+ The guardian-verify-setup skill manages the full outbound verification flow for Slack, including:
149
+
150
+ - Collecting the user's Slack user ID as the destination
151
+ - Starting the outbound verification session via the gateway endpoint `POST /v1/integrations/guardian/outbound/start` with `channel: "slack"`
152
+ - Sending a verification code via Slack DM
153
+ - Auto-polling for completion (the guardian-verify-setup skill handles this)
154
+ - Checking guardian status to confirm the binding was created
155
+
156
+ Tell the user: _"I've loaded the guardian verification guide. It will walk you through linking your Slack account as the trusted guardian."_
157
+
158
+ After the guardian-verify-setup skill completes (or the user skips), continue to Step 6.
159
+
160
+ **Note:** Guardian verification is optional but recommended. If the user declines or wants to skip, proceed to Step 6 without blocking.
161
+
162
+ ### Step 6: Verify Connection & Report Success
163
+
164
+ Check the final connection status:
165
+
166
+ ```bash
167
+ curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/slack/channel/config" \
168
+ -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
169
+ ```
170
+
171
+ Check guardian binding status:
172
+
173
+ ```bash
174
+ vellum integrations guardian status --channel slack --json
175
+ ```
176
+
177
+ The Settings > Channels > Slack card auto-refreshes on view appear via `fetchSlackChannelConfig()`, so the user will see the "Connected" status badge when they open or re-open that page.
178
+
179
+ Summarize what was done:
180
+
181
+ - Bot connected: {username} in {workspace}
182
+ - Socket Mode: active
183
+ - Guardian: {verified | not configured}
184
+ - Usage: "@{botUsername} in any channel, or DM the bot directly"
185
+
186
+ Tell the user: "Your Slack bot is now connected! Open **Settings > Channels** to see it reflected there."
187
+
188
+ ## Automated vs Manual Steps
189
+
190
+ | Step | Status | Details |
191
+ | ----------------------- | ---------------------------- | ------------------------------------------------------------- |
192
+ | App manifest generation | Automated | Skill generates manifest JSON with all scopes/events/settings |
193
+ | App creation | Manual (one-click) | User clicks manifest URL, selects workspace, creates |
194
+ | App token generation | Manual | User generates in Slack settings (can't be automated) |
195
+ | App installation | Manual | User clicks "Install to Workspace" |
196
+ | Token collection | Manual (secure prompt) | Via `credential_store` — never plaintext |
197
+ | Token validation | Automated | `auth.test` validates bot token on submission |
198
+ | Socket Mode connection | Automated | Gateway connects when tokens are stored |
199
+ | Routing | Automated (single-assistant) | CLI sets defaults |
200
+ | Guardian verification | Semi-automated | Via `guardian-verify-setup` skill |
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "objective": {
17
17
  "type": "string",
18
- "description": "The task objective what the subagent should accomplish"
18
+ "description": "The task objective \u2014 what the subagent should accomplish"
19
19
  },
20
20
  "context": {
21
21
  "type": "string",
@@ -24,6 +24,10 @@
24
24
  "send_result_to_user": {
25
25
  "type": "boolean",
26
26
  "description": "Whether to present the subagent's result to the user when it completes. Defaults to true. Set to false for internal/silent processing."
27
+ },
28
+ "reason": {
29
+ "type": "string",
30
+ "description": "Brief non-technical explanation of why this tool is being called"
27
31
  }
28
32
  },
29
33
  "required": ["label", "objective"]
@@ -33,7 +37,7 @@
33
37
  },
34
38
  {
35
39
  "name": "subagent_status",
36
- "description": "Get the status of a specific subagent or list all subagents for the current session. Only use this when the user explicitly asks about subagent status do NOT poll automatically, as you will be notified when subagents complete.",
40
+ "description": "Get the status of a specific subagent or list all subagents for the current session. Only use this when the user explicitly asks about subagent status \u2014 do NOT poll automatically, as you will be notified when subagents complete.",
37
41
  "category": "orchestration",
38
42
  "risk": "low",
39
43
  "input_schema": {
@@ -42,6 +46,10 @@
42
46
  "subagent_id": {
43
47
  "type": "string",
44
48
  "description": "Optional subagent ID to query. If omitted, returns all subagents for this session."
49
+ },
50
+ "reason": {
51
+ "type": "string",
52
+ "description": "Brief non-technical explanation of why this tool is being called"
45
53
  }
46
54
  },
47
55
  "required": []
@@ -60,6 +68,10 @@
60
68
  "subagent_id": {
61
69
  "type": "string",
62
70
  "description": "The ID of the subagent to abort."
71
+ },
72
+ "reason": {
73
+ "type": "string",
74
+ "description": "Brief non-technical explanation of why this tool is being called"
63
75
  }
64
76
  },
65
77
  "required": ["subagent_id"]
@@ -82,6 +94,10 @@
82
94
  "content": {
83
95
  "type": "string",
84
96
  "description": "The message content to send to the subagent."
97
+ },
98
+ "reason": {
99
+ "type": "string",
100
+ "description": "Brief non-technical explanation of why this tool is being called"
85
101
  }
86
102
  },
87
103
  "required": ["subagent_id", "content"]
@@ -100,6 +116,10 @@
100
116
  "subagent_id": {
101
117
  "type": "string",
102
118
  "description": "The ID of the subagent whose output to read."
119
+ },
120
+ "reason": {
121
+ "type": "string",
122
+ "description": "Brief non-technical explanation of why this tool is being called"
103
123
  }
104
124
  },
105
125
  "required": ["subagent_id"]