@inkeep/agents-work-apps 0.50.5 → 0.51.0

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.
@@ -4,7 +4,7 @@ import { getSlackClient } from "./services/client.js";
4
4
  import { sendResponseUrlMessage } from "./services/events/utils.js";
5
5
  import { SLACK_SPAN_KEYS } from "./tracer.js";
6
6
  import { handleAppMention } from "./services/events/app-mention.js";
7
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./services/events/block-actions.js";
7
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./services/events/block-actions.js";
8
8
  import { handleFollowUpSubmission, handleModalSubmission } from "./services/events/modal-submission.js";
9
9
  import "./services/events/index.js";
10
10
  import "./services/index.js";
@@ -176,6 +176,29 @@ async function dispatchSlackEvent(eventType, payload, options, span) {
176
176
  }
177
177
  })());
178
178
  }
179
+ if ((action.action_id === "tool_approval_approve" || action.action_id === "tool_approval_deny") && action.value) {
180
+ anyHandled = true;
181
+ const approved = action.action_id === "tool_approval_approve";
182
+ const slackUserId = payload.user?.id || "";
183
+ logger.info({
184
+ teamId,
185
+ actionId: action.action_id,
186
+ approved
187
+ }, `Handling block_action: ${action.action_id}`);
188
+ registerBackgroundWork(handleToolApproval({
189
+ actionValue: action.value,
190
+ approved,
191
+ teamId,
192
+ slackUserId,
193
+ responseUrl
194
+ }).catch((err) => {
195
+ const errorMessage = err instanceof Error ? err.message : String(err);
196
+ logger.error({
197
+ errorMessage,
198
+ actionId: action.action_id
199
+ }, "Failed to handle tool approval");
200
+ }).finally(() => flushTraces()));
201
+ }
179
202
  if (action.action_id === "open_follow_up_modal" && action.value && triggerId) {
180
203
  anyHandled = true;
181
204
  logger.info({
@@ -45,6 +45,7 @@ declare const SlackStrings: {
45
45
  };
46
46
  readonly status: {
47
47
  readonly thinking: (agentName: string) => string;
48
+ readonly readingThread: (agentName: string) => string;
48
49
  readonly noAgentsAvailable: "No agents available";
49
50
  readonly noProjectsConfigured: "No projects configured. Set up projects in the dashboard.";
50
51
  };
@@ -41,6 +41,7 @@ const SlackStrings = {
41
41
  usage: { mentionEmpty: "*Include a message to use your Inkeep agent:*\n\n• `@Inkeep <message>` — Message the default agent (reply appears in a thread)\n• `@Inkeep <message>` in a thread — Includes thread as context\n• `@Inkeep` in a thread — Uses the full thread as context\n\nUse `/inkeep help` for all available commands." },
42
42
  status: {
43
43
  thinking: (agentName) => `_${agentName} is thinking..._`,
44
+ readingThread: (agentName) => `_${agentName} is reading this thread..._`,
44
45
  noAgentsAvailable: "No agents available",
45
46
  noProjectsConfigured: "No projects configured. Set up projects in the dashboard."
46
47
  },
@@ -131,12 +131,15 @@ app.openapi(createProtectedRoute({
131
131
  tenantId
132
132
  });
133
133
  }
134
- if (existingLink) logger.info({
135
- slackUserId,
136
- existingUserId: existingLink.inkeepUserId,
137
- newUserId: inkeepUserId,
138
- tenantId
139
- }, "Slack user already linked, updating to new user");
134
+ if (existingLink) {
135
+ logger.info({
136
+ slackUserId,
137
+ existingUserId: existingLink.inkeepUserId,
138
+ newUserId: inkeepUserId,
139
+ tenantId
140
+ }, "Slack user already linked, updating to new user");
141
+ await deleteWorkAppSlackUserMapping(runDbClient_default)(tenantId, slackUserId, teamId, "work-apps-slack");
142
+ }
140
143
  const slackUserMapping = await createWorkAppSlackUserMapping(runDbClient_default)({
141
144
  tenantId,
142
145
  clientId: "work-apps-slack",
@@ -162,10 +165,9 @@ app.openapi(createProtectedRoute({
162
165
  tenantId
163
166
  });
164
167
  } catch (error) {
165
- const errorMessage = error instanceof Error ? error.message : String(error);
166
- if (errorMessage.includes("duplicate key") || errorMessage.includes("unique constraint")) {
167
- logger.warn({ userId: body.userId }, "Slack user already linked");
168
- return c.json({ error: "This Slack account is already linked to an Inkeep account." }, 409);
168
+ if (error instanceof Error && (error.message.includes("duplicate key") || error.message.includes("unique constraint")) || typeof error === "object" && error !== null && "cause" in error && typeof error.cause === "object" && error.cause?.code === "23505") {
169
+ logger.info({ userId: body.userId }, "Concurrent link resolved — mapping already exists");
170
+ return c.json({ success: true });
169
171
  }
170
172
  logger.error({
171
173
  error,
@@ -5,7 +5,7 @@ import { getSlackChannels, getSlackClient, revokeSlackToken } from "../services/
5
5
  import "../services/index.js";
6
6
  import { requireChannelMemberOrAdmin, requireWorkspaceAdmin } from "../middleware/permissions.js";
7
7
  import { OpenAPIHono, z } from "@hono/zod-openapi";
8
- import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
8
+ import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, findWorkAppSlackWorkspaceByTeamId, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, updateWorkAppSlackWorkspace, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
9
9
  import { createProtectedRoute, inheritedWorkAppsAuth } from "@inkeep/agents-core/middleware";
10
10
 
11
11
  //#region src/slack/routes/workspaces.ts
@@ -48,6 +48,7 @@ const ChannelAgentConfigSchema = z.object({
48
48
  grantAccessToMembers: z.boolean().optional()
49
49
  });
50
50
  const WorkspaceSettingsSchema = z.object({ defaultAgent: ChannelAgentConfigSchema.optional() });
51
+ const JoinFromWorkspaceSettingsSchema = z.object({ shouldAllowJoinFromWorkspace: z.boolean() });
51
52
  app.openapi(createProtectedRoute({
52
53
  method: "get",
53
54
  path: "/",
@@ -222,6 +223,89 @@ app.openapi(createProtectedRoute({
222
223
  }
223
224
  return c.json({ success: true });
224
225
  });
226
+ app.openapi(createProtectedRoute({
227
+ method: "get",
228
+ path: "/{teamId}/join-from-workspace",
229
+ summary: "Get Join From Workspace Setting",
230
+ description: "Get the join from workspace setting for the workspace",
231
+ operationId: "slack-get-join-from-workspace",
232
+ tags: [
233
+ "Work Apps",
234
+ "Slack",
235
+ "Workspaces"
236
+ ],
237
+ permission: inheritedWorkAppsAuth(),
238
+ request: { params: z.object({ teamId: z.string() }) },
239
+ responses: {
240
+ 200: {
241
+ description: "Join from workspace setting",
242
+ content: { "application/json": { schema: JoinFromWorkspaceSettingsSchema } }
243
+ },
244
+ 404: { description: "Workspace not found" }
245
+ }
246
+ }), async (c) => {
247
+ const { teamId } = c.req.valid("param");
248
+ const sessionTenantId = c.get("tenantId");
249
+ if (!sessionTenantId) return c.json({ error: "Unauthorized" }, 401);
250
+ const workspace = await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(sessionTenantId, teamId);
251
+ if (!workspace) return c.json({ shouldAllowJoinFromWorkspace: false });
252
+ return c.json({ shouldAllowJoinFromWorkspace: workspace.shouldAllowJoinFromWorkspace ?? false });
253
+ });
254
+ app.openapi(createProtectedRoute({
255
+ method: "put",
256
+ path: "/{teamId}/join-from-workspace",
257
+ summary: "Update Join From Workspace Setting",
258
+ description: "Enable or disable join from workspace for the workspace",
259
+ operationId: "slack-update-join-from-workspace",
260
+ tags: [
261
+ "Work Apps",
262
+ "Slack",
263
+ "Workspaces"
264
+ ],
265
+ permission: requireWorkspaceAdmin(),
266
+ request: {
267
+ params: z.object({ teamId: z.string() }),
268
+ body: { content: { "application/json": { schema: JoinFromWorkspaceSettingsSchema } } }
269
+ },
270
+ responses: {
271
+ 200: {
272
+ description: "Join from workspace setting updated",
273
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
274
+ },
275
+ 401: { description: "Unauthorized" },
276
+ 404: { description: "Workspace not found" },
277
+ 500: { description: "Failed to update setting" }
278
+ }
279
+ }), async (c) => {
280
+ const { teamId } = c.req.valid("param");
281
+ const { shouldAllowJoinFromWorkspace } = c.req.valid("json");
282
+ const sessionTenantId = c.get("tenantId");
283
+ if (!sessionTenantId) return c.json({ error: "Unauthorized" }, 401);
284
+ const workspace = await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(sessionTenantId, teamId);
285
+ if (!workspace) return c.json({ error: "Workspace not found" }, 404);
286
+ try {
287
+ if (!await updateWorkAppSlackWorkspace(runDbClient_default)(workspace.id, { shouldAllowJoinFromWorkspace })) {
288
+ logger.error({
289
+ teamId,
290
+ shouldAllowJoinFromWorkspace
291
+ }, "Failed to update join from workspace setting");
292
+ return c.json({ error: "Failed to update setting" }, 500);
293
+ }
294
+ logger.info({
295
+ teamId,
296
+ shouldAllowJoinFromWorkspace,
297
+ workspaceId: workspace.id
298
+ }, "Updated workspace join from workspace settings");
299
+ return c.json({ success: true });
300
+ } catch (error) {
301
+ logger.error({
302
+ teamId,
303
+ shouldAllowJoinFromWorkspace,
304
+ error
305
+ }, "Failed to update join from workspace setting");
306
+ return c.json({ error: "Failed to update setting" }, 500);
307
+ }
308
+ });
225
309
  app.openapi(createProtectedRoute({
226
310
  method: "delete",
227
311
  path: "/{teamId}",
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import * as slack_block_builder0 from "slack-block-builder";
2
3
 
3
4
  //#region src/slack/services/blocks/index.d.ts
@@ -63,6 +64,52 @@ interface AgentConfigSources {
63
64
  } | null;
64
65
  }
65
66
  declare function createStatusMessage(email: string, linkedAt: string, dashboardUrl: string, agentConfigs: AgentConfigSources): Readonly<slack_block_builder0.SlackMessageDto>;
67
+ interface ToolApprovalButtonValue {
68
+ toolCallId: string;
69
+ conversationId: string;
70
+ projectId: string;
71
+ agentId: string;
72
+ slackUserId: string;
73
+ channel: string;
74
+ threadTs: string;
75
+ toolName: string;
76
+ }
77
+ declare const ToolApprovalButtonValueSchema: z.ZodObject<{
78
+ toolCallId: z.ZodString;
79
+ conversationId: z.ZodString;
80
+ projectId: z.ZodString;
81
+ agentId: z.ZodString;
82
+ slackUserId: z.ZodString;
83
+ channel: z.ZodString;
84
+ threadTs: z.ZodString;
85
+ toolName: z.ZodString;
86
+ }, z.core.$strip>;
87
+ declare function buildToolApprovalBlocks(params: {
88
+ toolName: string;
89
+ input?: Record<string, unknown>;
90
+ buttonValue: string;
91
+ }): any[];
92
+ declare function buildToolApprovalDoneBlocks(params: {
93
+ toolName: string;
94
+ approved: boolean;
95
+ actorUserId: string;
96
+ }): {
97
+ type: string;
98
+ elements: {
99
+ type: string;
100
+ text: string;
101
+ }[];
102
+ }[];
103
+ declare function buildToolApprovalExpiredBlocks(params: {
104
+ toolName: string;
105
+ }): {
106
+ type: string;
107
+ elements: {
108
+ type: string;
109
+ text: string;
110
+ }[];
111
+ }[];
66
112
  declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
113
+ declare function createCreateInkeepAccountMessage(acceptUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
67
114
  //#endregion
68
- export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
115
+ export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildConversationResponseBlocks, buildFollowUpButton, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -1,4 +1,5 @@
1
1
  import { SlackStrings } from "../../i18n/strings.js";
2
+ import { z } from "zod";
2
3
  import { Blocks, Elements, Md, Message } from "slack-block-builder";
3
4
 
4
5
  //#region src/slack/services/blocks/index.ts
@@ -34,24 +35,14 @@ function buildFollowUpButton(params) {
34
35
  * Shows the user's message, a divider, the agent response, context, and a Follow Up button.
35
36
  */
36
37
  function buildConversationResponseBlocks(params) {
37
- const { userMessage, responseText, agentName, isError, followUpParams } = params;
38
- const blocks = [
39
- {
40
- type: "context",
41
- elements: [{
42
- type: "mrkdwn",
43
- text: `*You:* ${userMessage.length > 200 ? `${userMessage.slice(0, 200)}...` : userMessage}`
44
- }]
45
- },
46
- { type: "divider" },
47
- {
48
- type: "section",
49
- text: {
50
- type: "mrkdwn",
51
- text: responseText
52
- }
38
+ const { responseText, agentName, isError, followUpParams } = params;
39
+ const blocks = [{
40
+ type: "section",
41
+ text: {
42
+ type: "mrkdwn",
43
+ text: responseText
53
44
  }
54
- ];
45
+ }];
55
46
  if (!isError) {
56
47
  const contextBlock = createContextBlock({
57
48
  agentName,
@@ -66,7 +57,7 @@ function buildConversationResponseBlocks(params) {
66
57
  return blocks;
67
58
  }
68
59
  function createUpdatedHelpMessage() {
69
- return Message().blocks(Blocks.Section().text(`${Md.bold(SlackStrings.help.title)}`), Blocks.Section().text(SlackStrings.help.publicSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.privateSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.otherCommands), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.docsLink)).buildToObject();
60
+ return Message().blocks(Blocks.Header().text(SlackStrings.help.title), Blocks.Section().text(SlackStrings.help.publicSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.privateSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.otherCommands), Blocks.Divider(), Blocks.Context().elements(SlackStrings.help.docsLink)).buildToObject();
70
61
  }
71
62
  function createAlreadyLinkedMessage(email, linkedAt, dashboardUrl) {
72
63
  return Message().blocks(Blocks.Section().text(Md.bold("Already linked") + "\n\nYour Slack account is connected to Inkeep.\n\n" + Md.bold("Account:") + ` ${email}\n` + Md.bold("Linked:") + ` ${new Date(linkedAt).toLocaleDateString()}\n\nTo switch accounts, first run \`/inkeep unlink\``), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
@@ -84,9 +75,95 @@ function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
84
75
  else agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
85
76
  return Message().blocks(Blocks.Section().text(Md.bold("Connected to Inkeep") + `\n\n${Md.bold("Account:")} ${email}\n${Md.bold("Linked:")} ${new Date(linkedAt).toLocaleDateString()}\n` + agentLine), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
86
77
  }
78
+ const ToolApprovalButtonValueSchema = z.object({
79
+ toolCallId: z.string(),
80
+ conversationId: z.string(),
81
+ projectId: z.string(),
82
+ agentId: z.string(),
83
+ slackUserId: z.string(),
84
+ channel: z.string(),
85
+ threadTs: z.string(),
86
+ toolName: z.string()
87
+ });
88
+ function buildToolApprovalBlocks(params) {
89
+ const { toolName, input, buttonValue } = params;
90
+ const blocks = [{
91
+ type: "header",
92
+ text: {
93
+ type: "plain_text",
94
+ text: "Tool Approval Required",
95
+ emoji: false
96
+ }
97
+ }, {
98
+ type: "section",
99
+ text: {
100
+ type: "mrkdwn",
101
+ text: `The agent wants to use \`${toolName}\`.`
102
+ }
103
+ }];
104
+ if (input && Object.keys(input).length > 0) {
105
+ const jsonStr = JSON.stringify(input, null, 2);
106
+ const truncated = jsonStr.length > 2900 ? `${jsonStr.slice(0, 2900)}…` : jsonStr;
107
+ blocks.push({
108
+ type: "section",
109
+ text: {
110
+ type: "mrkdwn",
111
+ text: `\`\`\`json\n${truncated}\n\`\`\``
112
+ }
113
+ });
114
+ }
115
+ blocks.push({ type: "divider" });
116
+ blocks.push({
117
+ type: "actions",
118
+ elements: [{
119
+ type: "button",
120
+ text: {
121
+ type: "plain_text",
122
+ text: "Approve",
123
+ emoji: false
124
+ },
125
+ style: "primary",
126
+ action_id: "tool_approval_approve",
127
+ value: buttonValue
128
+ }, {
129
+ type: "button",
130
+ text: {
131
+ type: "plain_text",
132
+ text: "Deny",
133
+ emoji: false
134
+ },
135
+ style: "danger",
136
+ action_id: "tool_approval_deny",
137
+ value: buttonValue
138
+ }]
139
+ });
140
+ return blocks;
141
+ }
142
+ function buildToolApprovalDoneBlocks(params) {
143
+ const { toolName, approved, actorUserId } = params;
144
+ return [{
145
+ type: "context",
146
+ elements: [{
147
+ type: "mrkdwn",
148
+ text: approved ? `✅ Approved \`${toolName}\` · <@${actorUserId}>` : `❌ Denied \`${toolName}\` · <@${actorUserId}>`
149
+ }]
150
+ }];
151
+ }
152
+ function buildToolApprovalExpiredBlocks(params) {
153
+ return [{
154
+ type: "context",
155
+ elements: [{
156
+ type: "mrkdwn",
157
+ text: `⏱️ Expired · \`${params.toolName}\``
158
+ }]
159
+ }];
160
+ }
87
161
  function createJwtLinkMessage(linkUrl, expiresInMinutes) {
88
162
  return Message().blocks(Blocks.Section().text(`${Md.bold("Link your Inkeep account")}\n\nConnect your Slack and Inkeep accounts to use Inkeep agents.`), Blocks.Actions().elements(Elements.Button().text("Link Account").url(linkUrl).actionId("link_account").primary()), Blocks.Context().elements(`This link expires in ${expiresInMinutes} minutes.`)).buildToObject();
89
163
  }
164
+ function createCreateInkeepAccountMessage(acceptUrl, expiresInMinutes) {
165
+ return Message().blocks(Blocks.Section().text(`${Md.bold("Create your Inkeep account")}\n\nYou've been invited to join Inkeep. Create an account to start using Inkeep agents in Slack.`), Blocks.Actions().elements(Elements.Button().text("Create Account").url(acceptUrl).actionId("create_account").primary()), Blocks.Context().elements(`This link expires in ${expiresInMinutes} minutes.`)).buildToObject();
166
+ }
90
167
 
91
168
  //#endregion
92
- export { buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
169
+ export { ToolApprovalButtonValueSchema, buildConversationResponseBlocks, buildFollowUpButton, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -2,7 +2,7 @@ import { SlackWorkspaceConnection } from "../nango.js";
2
2
  import { SlackCommandPayload, SlackCommandResponse } from "../types.js";
3
3
 
4
4
  //#region src/slack/services/commands/index.d.ts
5
- declare function handleLinkCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
5
+ declare function handleLinkCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string, botToken?: string): Promise<SlackCommandResponse>;
6
6
  declare function handleUnlinkCommand(payload: SlackCommandPayload, tenantId: string): Promise<SlackCommandResponse>;
7
7
  declare function handleStatusCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
8
8
  declare function handleHelpCommand(): Promise<SlackCommandResponse>;
@@ -4,22 +4,116 @@ import runDbClient_default from "../../../db/runDbClient.js";
4
4
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
5
5
  import { resolveEffectiveAgent } from "../agent-resolution.js";
6
6
  import { SlackStrings } from "../../i18n/strings.js";
7
- import { createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
7
+ import { createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
8
  import { getSlackClient } from "../client.js";
9
9
  import { extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
10
10
  import { buildAgentSelectorModal } from "../modals.js";
11
- import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getInProcessFetch, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
11
+ import { createInvitationInDb, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, findWorkAppSlackWorkspaceByTeamId, flushTraces, getInProcessFetch, getOrganizationMemberByEmail, getPendingInvitationsByEmail, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
12
12
 
13
13
  //#region src/slack/services/commands/index.ts
14
14
  const DEFAULT_CLIENT_ID = "work-apps-slack";
15
15
  const LINK_CODE_TTL_MINUTES = 10;
16
16
  const logger = getLogger("slack-commands");
17
- async function handleLinkCommand(payload, dashboardUrl, tenantId) {
17
+ /**
18
+ * Create an invitation for a Slack user who doesn't have an Inkeep account yet.
19
+ * Returns the invitation ID and email so the caller can direct the user
20
+ * to the accept-invitation page.
21
+ *
22
+ * Returns null if:
23
+ * - Workspace doesn't have shouldAllowJoinFromWorkspace enabled
24
+ * - User already has an Inkeep account (JWT link flow is sufficient)
25
+ * - Service account is not configured
26
+ */
27
+ async function tryAutoInvite(payload, tenantId, botToken) {
28
+ try {
29
+ if (!(await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(tenantId, payload.teamId))?.shouldAllowJoinFromWorkspace) return null;
30
+ const slackClient = getSlackClient(botToken);
31
+ let userEmail;
32
+ try {
33
+ userEmail = (await slackClient.users.info({ user: payload.userId })).user?.profile?.email;
34
+ } catch (error) {
35
+ logger.warn({
36
+ error,
37
+ userId: payload.userId
38
+ }, "Failed to get user info from Slack");
39
+ return null;
40
+ }
41
+ if (!userEmail) {
42
+ logger.warn({ userId: payload.userId }, "No email found in Slack user profile");
43
+ return null;
44
+ }
45
+ if (await getOrganizationMemberByEmail(runDbClient_default)(tenantId, userEmail)) {
46
+ logger.debug({
47
+ userId: payload.userId,
48
+ email: userEmail
49
+ }, "User already has Inkeep account, skipping auto-invite");
50
+ return null;
51
+ }
52
+ const existingInvitation = (await getPendingInvitationsByEmail(runDbClient_default)(userEmail)).find((inv) => inv.organizationId === tenantId);
53
+ if (existingInvitation) {
54
+ logger.info({
55
+ userId: payload.userId,
56
+ tenantId,
57
+ invitationId: existingInvitation.id,
58
+ email: userEmail
59
+ }, "Reusing existing pending invitation for Slack user");
60
+ return {
61
+ invitationId: existingInvitation.id,
62
+ email: userEmail
63
+ };
64
+ }
65
+ const invitation = await createInvitationInDb(runDbClient_default)({
66
+ organizationId: tenantId,
67
+ email: userEmail
68
+ });
69
+ logger.info({
70
+ userId: payload.userId,
71
+ tenantId,
72
+ invitationId: invitation.id,
73
+ email: userEmail
74
+ }, "Invitation created for Slack user without Inkeep account");
75
+ return {
76
+ invitationId: invitation.id,
77
+ email: userEmail
78
+ };
79
+ } catch (error) {
80
+ logger.warn({
81
+ error,
82
+ userId: payload.userId,
83
+ tenantId
84
+ }, "Auto-invite attempt failed");
85
+ return null;
86
+ }
87
+ }
88
+ async function handleLinkCommand(payload, dashboardUrl, tenantId, botToken) {
18
89
  const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
19
90
  if (existingLink) return {
20
91
  response_type: "ephemeral",
21
92
  ...createAlreadyLinkedMessage(existingLink.slackEmail || existingLink.slackUsername || "Unknown", existingLink.linkedAt, dashboardUrl)
22
93
  };
94
+ if (botToken) {
95
+ const autoInvite = await tryAutoInvite(payload, tenantId, botToken);
96
+ if (autoInvite) {
97
+ const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
98
+ const linkToken = await signSlackLinkToken({
99
+ tenantId,
100
+ slackTeamId: payload.teamId,
101
+ slackUserId: payload.userId,
102
+ slackEnterpriseId: payload.enterpriseId,
103
+ slackUsername: payload.userName
104
+ });
105
+ const linkReturnUrl = `/link?token=${encodeURIComponent(linkToken)}`;
106
+ const acceptUrl = `${manageUiUrl}/accept-invitation/${autoInvite.invitationId}?email=${encodeURIComponent(autoInvite.email)}&returnUrl=${encodeURIComponent(linkReturnUrl)}`;
107
+ logger.info({
108
+ invitationId: autoInvite.invitationId,
109
+ email: autoInvite.email
110
+ }, "Directing new user to accept-invitation page with link returnUrl");
111
+ return {
112
+ response_type: "ephemeral",
113
+ ...createCreateInkeepAccountMessage(acceptUrl, LINK_CODE_TTL_MINUTES)
114
+ };
115
+ }
116
+ }
23
117
  try {
24
118
  const linkToken = await signSlackLinkToken({
25
119
  tenantId,
@@ -363,7 +457,7 @@ async function handleCommand(payload) {
363
457
  }, "Slack command received");
364
458
  switch (subcommand) {
365
459
  case "link":
366
- case "connect": return handleLinkCommand(payload, dashboardUrl, tenantId);
460
+ case "connect": return handleLinkCommand(payload, dashboardUrl, tenantId, workspaceConnection.botToken);
367
461
  case "status": return handleStatusCommand(payload, dashboardUrl, tenantId);
368
462
  case "unlink":
369
463
  case "logout":
@@ -196,7 +196,7 @@ async function handleAppMention(params) {
196
196
  thinkingMessageTs = (await slackClient.chat.postMessage({
197
197
  channel,
198
198
  thread_ts: threadTs,
199
- text: `_${agentDisplayName} is reading this thread..._`
199
+ text: SlackStrings.status.readingThread(agentDisplayName)
200
200
  })).ts || void 0;
201
201
  const conversationId$1 = generateSlackConversationId({
202
202
  teamId,
@@ -274,7 +274,7 @@ Respond naturally as if you're joining the conversation to help.`;
274
274
  thinkingMessageTs = (await slackClient.chat.postMessage({
275
275
  channel,
276
276
  thread_ts: replyThreadTs,
277
- text: `_${agentDisplayName} is preparing a response..._`
277
+ text: SlackStrings.status.thinking(agentDisplayName)
278
278
  })).ts || void 0;
279
279
  const conversationId = generateSlackConversationId({
280
280
  teamId,
@@ -3,6 +3,17 @@
3
3
  * Handlers for Slack block action events (button clicks, selections, etc.)
4
4
  * and message shortcuts
5
5
  */
6
+ /**
7
+ * Handle tool approval/denial button clicks.
8
+ * Called when a user clicks "Approve" or "Deny" on a tool approval message.
9
+ */
10
+ declare function handleToolApproval(params: {
11
+ actionValue: string;
12
+ approved: boolean;
13
+ teamId: string;
14
+ slackUserId: string;
15
+ responseUrl?: string;
16
+ }): Promise<void>;
6
17
  /**
7
18
  * Handle opening the agent selector modal when user clicks "Select Agent" button
8
19
  */
@@ -37,4 +48,4 @@ declare function handleMessageShortcut(params: {
37
48
  responseUrl?: string;
38
49
  }): Promise<void>;
39
50
  //#endregion
40
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal };
51
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
@@ -1,10 +1,13 @@
1
+ import { env } from "../../../env.js";
1
2
  import { getLogger } from "../../../logger.js";
2
3
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
3
4
  import { SlackStrings } from "../../i18n/strings.js";
5
+ import { ToolApprovalButtonValueSchema, buildToolApprovalDoneBlocks } from "../blocks/index.js";
4
6
  import { getSlackClient } from "../client.js";
5
- import { fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
7
+ import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
6
8
  import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "../modals.js";
7
9
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
10
+ import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
8
11
 
9
12
  //#region src/slack/services/events/block-actions.ts
10
13
  /**
@@ -13,6 +16,127 @@ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../
13
16
  */
14
17
  const logger = getLogger("slack-block-actions");
15
18
  /**
19
+ * Handle tool approval/denial button clicks.
20
+ * Called when a user clicks "Approve" or "Deny" on a tool approval message.
21
+ */
22
+ async function handleToolApproval(params) {
23
+ return tracer.startActiveSpan(SLACK_SPAN_NAMES.TOOL_APPROVAL, async (span) => {
24
+ const { actionValue, approved, teamId, slackUserId, responseUrl } = params;
25
+ span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
26
+ span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
27
+ try {
28
+ const buttonValue = ToolApprovalButtonValueSchema.parse(JSON.parse(actionValue));
29
+ const { toolCallId, conversationId, projectId, agentId, toolName } = buttonValue;
30
+ span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
31
+ const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
32
+ if (!workspaceConnection?.botToken) {
33
+ logger.error({ teamId }, "No bot token for tool approval");
34
+ span.end();
35
+ return;
36
+ }
37
+ const tenantId = workspaceConnection.tenantId;
38
+ const slackClient = getSlackClient(workspaceConnection.botToken);
39
+ if (slackUserId !== buttonValue.slackUserId) {
40
+ await slackClient.chat.postEphemeral({
41
+ channel: buttonValue.channel,
42
+ user: slackUserId,
43
+ thread_ts: buttonValue.threadTs,
44
+ text: "Only the user who started this conversation can approve or deny this action."
45
+ }).catch((e) => logger.warn({ error: e }, "Failed to send ownership error notification"));
46
+ span.end();
47
+ return;
48
+ }
49
+ const existingLink = await findCachedUserMapping(tenantId, slackUserId, teamId);
50
+ if (!existingLink) {
51
+ await slackClient.chat.postEphemeral({
52
+ channel: buttonValue.channel,
53
+ user: slackUserId,
54
+ thread_ts: buttonValue.threadTs,
55
+ text: "You need to link your Inkeep account first. Use `/inkeep link`."
56
+ }).catch((e) => logger.warn({ error: e }, "Failed to send not-linked notification"));
57
+ span.end();
58
+ return;
59
+ }
60
+ const slackUserToken = await signSlackUserToken({
61
+ inkeepUserId: existingLink.inkeepUserId,
62
+ tenantId,
63
+ slackTeamId: teamId,
64
+ slackUserId
65
+ });
66
+ const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
67
+ const approvalResponse = await getInProcessFetch()(`${apiUrl}/run/api/chat`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: `Bearer ${slackUserToken}`,
72
+ "x-inkeep-project-id": projectId,
73
+ "x-inkeep-agent-id": agentId
74
+ },
75
+ body: JSON.stringify({
76
+ conversationId,
77
+ messages: [{
78
+ role: "tool",
79
+ parts: [{
80
+ type: "tool-call",
81
+ toolCallId,
82
+ state: "approval-responded",
83
+ approval: {
84
+ id: toolCallId,
85
+ approved
86
+ }
87
+ }]
88
+ }]
89
+ })
90
+ });
91
+ if (!approvalResponse.ok) {
92
+ const errorBody = await approvalResponse.text().catch(() => "");
93
+ logger.error({
94
+ status: approvalResponse.status,
95
+ errorBody,
96
+ toolCallId,
97
+ conversationId
98
+ }, "Tool approval API call failed");
99
+ await slackClient.chat.postEphemeral({
100
+ channel: buttonValue.channel,
101
+ user: slackUserId,
102
+ thread_ts: buttonValue.threadTs,
103
+ text: `Failed to ${approved ? "approve" : "deny"} \`${toolName}\`. Please try again.`
104
+ }).catch((e) => logger.warn({ error: e }, "Failed to send approval error notification"));
105
+ span.end();
106
+ return;
107
+ }
108
+ if (responseUrl) await sendResponseUrlMessage(responseUrl, {
109
+ text: approved ? `✅ Approved \`${toolName}\`` : `❌ Denied \`${toolName}\``,
110
+ replace_original: true,
111
+ blocks: buildToolApprovalDoneBlocks({
112
+ toolName,
113
+ approved,
114
+ actorUserId: slackUserId
115
+ })
116
+ }).catch((e) => logger.warn({ error: e }, "Failed to update approval message"));
117
+ logger.info({
118
+ toolCallId,
119
+ conversationId,
120
+ approved,
121
+ slackUserId
122
+ }, "Tool approval processed");
123
+ span.end();
124
+ } catch (error) {
125
+ if (error instanceof Error) setSpanWithError(span, error);
126
+ logger.error({
127
+ error,
128
+ teamId,
129
+ slackUserId
130
+ }, "Failed to handle tool approval");
131
+ if (responseUrl) await sendResponseUrlMessage(responseUrl, {
132
+ text: "Something went wrong processing your request. Please try again.",
133
+ response_type: "ephemeral"
134
+ }).catch((e) => logger.warn({ error: e }, "Failed to send error notification"));
135
+ span.end();
136
+ }
137
+ });
138
+ }
139
+ /**
16
140
  * Handle opening the agent selector modal when user clicks "Select Agent" button
17
141
  */
18
142
  async function handleOpenAgentSelectorModal(params) {
@@ -262,4 +386,4 @@ async function handleMessageShortcut(params) {
262
386
  }
263
387
 
264
388
  //#endregion
265
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal };
389
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
@@ -1,6 +1,6 @@
1
1
  import { InlineSelectorMetadata, handleAppMention } from "./app-mention.js";
2
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./block-actions.js";
2
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./block-actions.js";
3
3
  import { handleFollowUpSubmission, handleModalSubmission } from "./modal-submission.js";
4
4
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
5
5
  import { StreamResult, streamAgentResponse } from "./streaming.js";
6
- export { type InlineSelectorMetadata, SlackErrorType, type StreamResult, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
6
+ export { type InlineSelectorMetadata, SlackErrorType, type StreamResult, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
@@ -1,7 +1,7 @@
1
1
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
2
2
  import { streamAgentResponse } from "./streaming.js";
3
3
  import { handleAppMention } from "./app-mention.js";
4
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./block-actions.js";
4
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./block-actions.js";
5
5
  import { handleFollowUpSubmission, handleModalSubmission } from "./modal-submission.js";
6
6
 
7
- export { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
7
+ export { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
@@ -20,7 +20,7 @@ declare function streamAgentResponse(params: {
20
20
  agentId: string;
21
21
  question: string;
22
22
  agentName: string;
23
- conversationId?: string;
23
+ conversationId: string;
24
24
  }): Promise<StreamResult>;
25
25
  //#endregion
26
26
  export { StreamResult, streamAgentResponse };
@@ -1,6 +1,6 @@
1
1
  import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
- import { createContextBlock } from "../blocks/index.js";
3
+ import { buildToolApprovalBlocks, buildToolApprovalExpiredBlocks, createContextBlock } from "../blocks/index.js";
4
4
  import { SlackErrorType, classifyError, extractApiErrorMessage, getUserFriendlyErrorMessage } from "./utils.js";
5
5
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
6
6
  import { getInProcessFetch } from "@inkeep/agents-core";
@@ -50,6 +50,10 @@ async function streamAgentResponse(params) {
50
50
  projectId
51
51
  }, "Starting streaming agent response");
52
52
  const abortController = new AbortController();
53
+ const abortPromise = new Promise((_, reject) => {
54
+ abortController.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("Stream timeout")), { once: true });
55
+ });
56
+ let reader;
53
57
  const timeoutId = setTimeout(() => {
54
58
  logger.warn({
55
59
  channel,
@@ -177,7 +181,7 @@ async function streamAgentResponse(params) {
177
181
  errorMessage
178
182
  };
179
183
  }
180
- const reader = response.body.getReader();
184
+ reader = response.body.getReader();
181
185
  const decoder = new TextDecoder();
182
186
  let buffer = "";
183
187
  let fullText = "";
@@ -187,10 +191,11 @@ async function streamAgentResponse(params) {
187
191
  recipient_user_id: slackUserId,
188
192
  thread_ts: threadTs
189
193
  });
194
+ const pendingApprovalMessages = [];
190
195
  try {
191
196
  let agentCompleted = false;
192
197
  while (true) {
193
- const { done, value } = await reader.read();
198
+ const { done, value } = await Promise.race([reader.read(), abortPromise]);
194
199
  if (done) break;
195
200
  buffer += decoder.decode(value, { stream: true });
196
201
  const lines = buffer.split("\n");
@@ -208,6 +213,43 @@ async function streamAgentResponse(params) {
208
213
  }
209
214
  continue;
210
215
  }
216
+ if (data.type === "tool-approval-request" && conversationId) {
217
+ const toolName = data.toolName || "Tool";
218
+ const toolCallId = data.toolCallId;
219
+ const input = data.input;
220
+ const buttonValue = {
221
+ toolCallId,
222
+ conversationId,
223
+ projectId,
224
+ agentId,
225
+ slackUserId,
226
+ channel,
227
+ threadTs,
228
+ toolName
229
+ };
230
+ const approvalPost = await slackClient.chat.postMessage({
231
+ channel,
232
+ thread_ts: threadTs,
233
+ text: `Tool approval required: \`${toolName}\``,
234
+ blocks: buildToolApprovalBlocks({
235
+ toolName,
236
+ input,
237
+ buttonValue: JSON.stringify(buttonValue)
238
+ })
239
+ }).catch((e) => {
240
+ logger.warn({
241
+ error: e,
242
+ toolCallId
243
+ }, "Failed to post tool approval message");
244
+ return null;
245
+ });
246
+ if (approvalPost?.ts) pendingApprovalMessages.push({
247
+ messageTs: approvalPost.ts,
248
+ toolName
249
+ });
250
+ clearTimeout(timeoutId);
251
+ continue;
252
+ }
211
253
  if (data.type === "text-start" || data.type === "text-end") continue;
212
254
  if (data.type === "text-delta" && data.delta) {
213
255
  fullText += data.delta;
@@ -256,7 +298,17 @@ async function streamAgentResponse(params) {
256
298
  return { success: true };
257
299
  } catch (streamError) {
258
300
  clearTimeout(timeoutId);
301
+ reader?.cancel().catch(() => {});
259
302
  if (streamError instanceof Error) setSpanWithError(span, streamError);
303
+ for (const { messageTs, toolName } of pendingApprovalMessages) await slackClient.chat.update({
304
+ channel,
305
+ ts: messageTs,
306
+ text: `⏱️ Expired · \`${toolName}\``,
307
+ blocks: buildToolApprovalExpiredBlocks({ toolName })
308
+ }).catch((e) => logger.warn({
309
+ error: e,
310
+ messageTs
311
+ }, "Failed to expire approval message"));
260
312
  if (fullText.length > 0) {
261
313
  span.setAttribute(SLACK_SPAN_KEYS.CONTENT_ALREADY_DELIVERED, true);
262
314
  logger.warn({
@@ -275,6 +327,22 @@ async function streamAgentResponse(params) {
275
327
  span.end();
276
328
  return { success: true };
277
329
  }
330
+ if (pendingApprovalMessages.length > 0) {
331
+ for (const { toolName } of pendingApprovalMessages) await slackClient.chat.postMessage({
332
+ channel,
333
+ thread_ts: threadTs,
334
+ text: `Approval for \`${toolName}\` has expired.`
335
+ }).catch((e) => logger.warn({ error: e }, "Failed to send approval expired notification"));
336
+ await withTimeout(streamer.stop(), CLEANUP_TIMEOUT_MS, "streamer.stop-cleanup").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
337
+ if (thinkingMessageTs) try {
338
+ await slackClient.chat.delete({
339
+ channel,
340
+ ts: thinkingMessageTs
341
+ });
342
+ } catch {}
343
+ span.end();
344
+ return { success: true };
345
+ }
278
346
  logger.error({ streamError }, "Error during Slack streaming");
279
347
  await withTimeout(streamer.stop(), CLEANUP_TIMEOUT_MS, "streamer.stop-cleanup").catch((e) => logger.warn({ error: e }, "Failed to stop streamer during error cleanup"));
280
348
  if (thinkingMessageTs) try {
@@ -11,9 +11,9 @@ declare function findCachedUserMapping(tenantId: string, slackUserId: string, te
11
11
  id: string;
12
12
  createdAt: string;
13
13
  updatedAt: string;
14
+ slackUserId: string;
14
15
  tenantId: string;
15
16
  clientId: string;
16
- slackUserId: string;
17
17
  slackTeamId: string;
18
18
  slackEnterpriseId: string | null;
19
19
  inkeepUserId: string;
@@ -239,9 +239,12 @@ async function resolveChannelAgentConfig(teamId, channelId, workspace) {
239
239
  async function sendResponseUrlMessage(responseUrl, message) {
240
240
  try {
241
241
  const payload = { text: message.text };
242
- if (message.replace_original) payload.replace_original = true;
242
+ if (message.replace_original === true) payload.replace_original = true;
243
243
  else if (message.delete_original) payload.delete_original = true;
244
- else if (message.response_type) payload.response_type = message.response_type;
244
+ else {
245
+ payload.replace_original = false;
246
+ if (message.response_type) payload.response_type = message.response_type;
247
+ }
245
248
  if (message.blocks) payload.blocks = message.blocks;
246
249
  logger.info({
247
250
  hasBlocks: !!message.blocks,
@@ -1,11 +1,11 @@
1
1
  import { AgentResolutionParams, ResolvedAgentConfig, getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
2
- import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
2
+ import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildConversationResponseBlocks, buildFollowUpButton, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
3
3
  import { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
4
4
  import { DefaultAgentConfig, SlackWorkspaceConnection, WorkspaceInstallData, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
5
5
  import { SlackCommandPayload, SlackCommandResponse } from "./types.js";
6
6
  import { handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
7
7
  import { InlineSelectorMetadata, handleAppMention } from "./events/app-mention.js";
8
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
8
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./events/block-actions.js";
9
9
  import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
10
10
  import { AgentOption, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, FollowUpModalMetadata, ModalMetadata, buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
11
11
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
@@ -13,4 +13,4 @@ import { StreamResult, streamAgentResponse } from "./events/streaming.js";
13
13
  import "./events/index.js";
14
14
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
15
15
  import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
16
- export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, FollowUpButtonParams, FollowUpModalMetadata, InlineSelectorMetadata, ModalMetadata, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, WorkspaceInstallData, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
16
+ export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, FollowUpButtonParams, FollowUpModalMetadata, InlineSelectorMetadata, ModalMetadata, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, WorkspaceInstallData, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -1,16 +1,16 @@
1
1
  import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
2
2
  import { getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
3
- import { buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
3
+ import { ToolApprovalButtonValueSchema, buildConversationResponseBlocks, buildFollowUpButton, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
4
4
  import { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
5
5
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
6
6
  import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
7
7
  import { handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
8
8
  import { streamAgentResponse } from "./events/streaming.js";
9
9
  import { handleAppMention } from "./events/app-mention.js";
10
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
10
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./events/block-actions.js";
11
11
  import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
12
12
  import "./events/index.js";
13
13
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
14
14
  import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
15
15
 
16
- export { SlackErrorType, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
16
+ export { SlackErrorType, ToolApprovalButtonValueSchema, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -15,6 +15,7 @@ declare const SLACK_SPAN_NAMES: {
15
15
  readonly OPEN_FOLLOW_UP_MODAL: "slack.open_follow_up_modal";
16
16
  readonly PROJECT_SELECT_UPDATE: "slack.project_select_update";
17
17
  readonly CALL_AGENT_API: "slack.call_agent_api";
18
+ readonly TOOL_APPROVAL: "slack.tool_approval";
18
19
  };
19
20
  declare const SLACK_SPAN_KEYS: {
20
21
  readonly TEAM_ID: "slack.team_id";
@@ -13,7 +13,8 @@ const SLACK_SPAN_NAMES = {
13
13
  OPEN_AGENT_SELECTOR_MODAL: "slack.open_agent_selector_modal",
14
14
  OPEN_FOLLOW_UP_MODAL: "slack.open_follow_up_modal",
15
15
  PROJECT_SELECT_UPDATE: "slack.project_select_update",
16
- CALL_AGENT_API: "slack.call_agent_api"
16
+ CALL_AGENT_API: "slack.call_agent_api",
17
+ TOOL_APPROVAL: "slack.tool_approval"
17
18
  };
18
19
  const SLACK_SPAN_KEYS = {
19
20
  TEAM_ID: "slack.team_id",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.50.5",
3
+ "version": "0.51.0",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -33,7 +33,7 @@
33
33
  "jose": "^6.1.0",
34
34
  "minimatch": "^10.1.1",
35
35
  "slack-block-builder": "^2.8.0",
36
- "@inkeep/agents-core": "0.50.5"
36
+ "@inkeep/agents-core": "0.51.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@hono/zod-openapi": "^1.1.5",