@andreprado/agentkit 0.1.0-alpha.9 → 0.1.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.
- package/README.md +18 -1
- package/docs/guides/add-channel.md +251 -7
- package/docs/guides/add-knowledge.md +10 -0
- package/docs/guides/add-managed-composio.md +165 -0
- package/docs/guides/add-tool.md +10 -3
- package/docs/guides/channel-security.md +162 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +61 -1
- package/docs/guides/connect-whatsapp-evolution.md +121 -0
- package/docs/guides/connect-whatsapp-uazapi.md +139 -0
- package/docs/guides/connect-whatsapp-zapster.md +119 -16
- package/docs/guides/create-agent.md +31 -4
- package/docs/guides/debug-channel.md +159 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +32 -14
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +95 -25
- package/docs/guides/security-rules.md +9 -5
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-jev.md +67 -0
- package/docs/guides/use-provider.md +70 -3
- package/docs/llms-full.txt +295 -25
- package/docs/llms.txt +54 -7
- package/package.json +3 -7
- package/src/cli/args.ts +23 -2
- package/src/cli/cloud-client.ts +121 -9
- package/src/cli/commands/channels.ts +856 -36
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/provider.ts +47 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/deploy-chat-ui.ts +232 -18
- package/src/cli/deploy-readiness.ts +227 -14
- package/src/cli/help.ts +67 -9
- package/src/cli/index.ts +740 -35
- package/src/cli/new-command.ts +41 -0
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +18 -35
- package/src/index.ts +565 -11
- package/src/providers/codex-auth.ts +111 -0
- package/src/providers/pi.ts +88 -19
- package/src/providers/test.ts +36 -0
- package/src/providers/types.ts +8 -0
- package/src/runtime/channel-test-harness.ts +21 -1
- package/src/runtime/channels/discord.ts +896 -0
- package/src/runtime/channels/generic-webhook.ts +974 -0
- package/src/runtime/channels/slack.ts +646 -0
- package/src/runtime/channels/telegram.ts +343 -12
- package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
- package/src/runtime/channels/whatsapp-zapster.ts +674 -40
- package/src/runtime/channels.ts +83 -3
- package/src/runtime/chat.ts +70 -44
- package/src/runtime/config.ts +489 -20
- package/src/runtime/core/manifest.ts +75 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/deploy-readiness.ts +34 -4
- package/src/runtime/dev-server.ts +639 -39
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +445 -74
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +173 -4
- package/src/runtime/integrations/composio.ts +425 -0
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +71 -7
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/targets/cloudflare/build.ts +1010 -208
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +26 -2
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +7 -2
- package/src/templates/blank.ts +37 -9
- package/src/templates/dentista.ts +40 -14
- package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
- package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
- package/src/templates/skills/agentkit-database/SKILL.md +11 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
- package/src/templates/skills/agentkit-security/SKILL.md +5 -2
- package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
- package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
- package/src/templates/support.ts +42 -12
- package/docs/guides/agentkit-skills-architecture.md +0 -471
- package/docs/guides/channels-implementation-map.md +0 -243
- package/docs/guides/channels-production-handoff.md +0 -101
- package/docs/portable-deploy-release-checklist.md +0 -41
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelAdapter,
|
|
3
|
+
ChannelSendInput,
|
|
4
|
+
ChannelSendResult,
|
|
5
|
+
NormalizedChannelEvent,
|
|
6
|
+
RawWebhookEvent,
|
|
7
|
+
WebhookVerificationResult,
|
|
8
|
+
} from "../channels";
|
|
9
|
+
import { AgentKitError } from "../errors";
|
|
10
|
+
|
|
11
|
+
const SLACK_API_BASE_URL = "https://slack.com/api";
|
|
12
|
+
const SLACK_SIGNATURE_VERSION = "v0";
|
|
13
|
+
const SLACK_REPLAY_WINDOW_SECONDS = 60 * 5;
|
|
14
|
+
|
|
15
|
+
export const slackChannelAdapter: ChannelAdapter = {
|
|
16
|
+
type: "slack",
|
|
17
|
+
provider: "slack",
|
|
18
|
+
requiredSecrets: ["SLACK_BOT_TOKEN", "SLACK_SIGNING_SECRET"],
|
|
19
|
+
async verifyWebhook(input): Promise<WebhookVerificationResult> {
|
|
20
|
+
return verifySlackWebhook(input);
|
|
21
|
+
},
|
|
22
|
+
getWebhookResponse(input) {
|
|
23
|
+
const body = parseJsonObject(input);
|
|
24
|
+
|
|
25
|
+
if (readString(body, "type") !== "url_verification") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const challenge = readString(body, "challenge");
|
|
30
|
+
|
|
31
|
+
if (!challenge) {
|
|
32
|
+
throw new AgentKitError("channel_payload_invalid", "Slack url_verification payload is missing challenge.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
headers: {
|
|
37
|
+
"content-type": "text/plain; charset=utf-8",
|
|
38
|
+
},
|
|
39
|
+
body: challenge,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
normalizeInbound(input) {
|
|
43
|
+
const body = parseJsonObject(input);
|
|
44
|
+
const envelopeType = readString(body, "type");
|
|
45
|
+
|
|
46
|
+
if (envelopeType === "url_verification") {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const providerEventId = readString(body, "event_id");
|
|
51
|
+
const teamId = readString(body, "team_id") ?? "unknown";
|
|
52
|
+
|
|
53
|
+
if (envelopeType === "app_rate_limited") {
|
|
54
|
+
const minute = readNumber(body, "minute_rate_limited");
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
...slackBaseEvent(input, {
|
|
59
|
+
providerEventId: providerEventId ?? `rate_limited:${teamId}:${minute ?? input.receivedAt}`,
|
|
60
|
+
teamId,
|
|
61
|
+
}),
|
|
62
|
+
kind: "unsupported",
|
|
63
|
+
unsupportedReason: "slack_app_rate_limited",
|
|
64
|
+
providerMetadata: {
|
|
65
|
+
envelopeType,
|
|
66
|
+
...(minute !== undefined ? { minuteRateLimited: minute } : {}),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (envelopeType !== "event_callback") {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
...slackBaseEvent(input, {
|
|
76
|
+
providerEventId: providerEventId ?? `unsupported:${teamId}:${input.receivedAt}`,
|
|
77
|
+
teamId,
|
|
78
|
+
}),
|
|
79
|
+
kind: "unsupported",
|
|
80
|
+
unsupportedReason: `unsupported_envelope_type:${envelopeType ?? "unknown"}`,
|
|
81
|
+
providerMetadata: {
|
|
82
|
+
envelopeType,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!providerEventId) {
|
|
89
|
+
throw new AgentKitError("channel_payload_invalid", "Slack event_callback payload is missing event_id.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const event = readRecord(body, "event");
|
|
93
|
+
const eventType = readString(event, "type");
|
|
94
|
+
const subtype = readString(event, "subtype");
|
|
95
|
+
const botId = readString(event, "bot_id");
|
|
96
|
+
const channelId = readString(event, "channel");
|
|
97
|
+
const userId = readString(event, "user");
|
|
98
|
+
const text = readString(event, "text");
|
|
99
|
+
const messageTs = readString(event, "ts");
|
|
100
|
+
const threadTs = readString(event, "thread_ts");
|
|
101
|
+
const channelType = readString(event, "channel_type");
|
|
102
|
+
const isDirectMessage = eventType === "message" && channelType === "im";
|
|
103
|
+
const isAppMention = eventType === "app_mention";
|
|
104
|
+
const base = slackBaseEvent(input, {
|
|
105
|
+
providerEventId,
|
|
106
|
+
teamId,
|
|
107
|
+
channelId,
|
|
108
|
+
userId,
|
|
109
|
+
threadTs: isDirectMessage ? undefined : threadTs ?? messageTs,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (subtype || botId) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isAppMention && !isDirectMessage) {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
...base,
|
|
120
|
+
kind: "unsupported",
|
|
121
|
+
unsupportedReason: `unsupported_event_type:${eventType ?? "unknown"}`,
|
|
122
|
+
providerMetadata: slackProviderMetadata(body, event),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!channelId || !userId || !messageTs || text === undefined) {
|
|
128
|
+
throw new AgentKitError("channel_payload_invalid", "Slack message event is missing channel, user, ts, or text.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const content = isAppMention ? stripSlackBotMention(text) : text.trim();
|
|
132
|
+
|
|
133
|
+
if (!content) {
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
...base,
|
|
137
|
+
kind: "unsupported",
|
|
138
|
+
unsupportedReason: "empty_message",
|
|
139
|
+
providerMetadata: slackProviderMetadata(body, event),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
...base,
|
|
147
|
+
kind: "message",
|
|
148
|
+
providerMetadata: slackProviderMetadata(body, event),
|
|
149
|
+
replyTarget: {
|
|
150
|
+
type: "slack_message",
|
|
151
|
+
channelId,
|
|
152
|
+
messageTs,
|
|
153
|
+
...(threadTs || isAppMention ? { threadTs: threadTs ?? messageTs } : {}),
|
|
154
|
+
},
|
|
155
|
+
message: {
|
|
156
|
+
role: "user",
|
|
157
|
+
contentType: "text",
|
|
158
|
+
content,
|
|
159
|
+
providerMessageId: messageTs,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
},
|
|
164
|
+
async sendMessage(input) {
|
|
165
|
+
return sendSlackMessage(input);
|
|
166
|
+
},
|
|
167
|
+
getStatus(input) {
|
|
168
|
+
if (!input.secrets.SLACK_BOT_TOKEN) {
|
|
169
|
+
return {
|
|
170
|
+
status: "needs_setup",
|
|
171
|
+
checkedAt: new Date().toISOString(),
|
|
172
|
+
message: "Secret SLACK_BOT_TOKEN is missing.",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!input.secrets.SLACK_SIGNING_SECRET) {
|
|
177
|
+
return {
|
|
178
|
+
status: "needs_setup",
|
|
179
|
+
checkedAt: new Date().toISOString(),
|
|
180
|
+
message: "Secret SLACK_SIGNING_SECRET is missing.",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
status: "waiting_for_provider_event",
|
|
186
|
+
checkedAt: new Date().toISOString(),
|
|
187
|
+
message: "Paste the AgentKit webhook URL into the Slack app Event Subscriptions Request URL.",
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type SlackPostMessageRequest = {
|
|
193
|
+
method: "POST";
|
|
194
|
+
url: string;
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: string;
|
|
197
|
+
"Content-Type": "application/json";
|
|
198
|
+
};
|
|
199
|
+
body: {
|
|
200
|
+
channel: string;
|
|
201
|
+
text: string;
|
|
202
|
+
thread_ts?: string;
|
|
203
|
+
mrkdwn: boolean;
|
|
204
|
+
parse: "none";
|
|
205
|
+
link_names: boolean;
|
|
206
|
+
unfurl_links: boolean;
|
|
207
|
+
unfurl_media: boolean;
|
|
208
|
+
};
|
|
209
|
+
idempotencyKey: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type SlackFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
213
|
+
|
|
214
|
+
export function buildSlackPostMessageRequest(input: ChannelSendInput): SlackPostMessageRequest {
|
|
215
|
+
return buildSlackPostMessageRequestInternal(input, {
|
|
216
|
+
redactToken: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function sendSlackMessage(
|
|
221
|
+
input: ChannelSendInput,
|
|
222
|
+
fetcher: SlackFetch = fetch,
|
|
223
|
+
): Promise<ChannelSendResult> {
|
|
224
|
+
if (input.replyTarget?.type !== "slack_message") {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
retryable: false,
|
|
228
|
+
code: "channel_send_failed",
|
|
229
|
+
message: "Slack sendMessage requires a Slack message reply target.",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
234
|
+
try {
|
|
235
|
+
const request = buildSlackPostMessageRequest(input);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
status: "stubbed",
|
|
240
|
+
providerRequestId: request.idempotencyKey,
|
|
241
|
+
providerMetadata: {
|
|
242
|
+
method: request.method,
|
|
243
|
+
url: request.url,
|
|
244
|
+
headers: request.headers,
|
|
245
|
+
body: request.body,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
retryable: false,
|
|
252
|
+
code: "channel_send_failed",
|
|
253
|
+
message: error instanceof Error ? error.message : String(error),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!input.secrets.SLACK_BOT_TOKEN) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
retryable: false,
|
|
262
|
+
code: "channel_secret_missing",
|
|
263
|
+
message: "Secret SLACK_BOT_TOKEN is not set for this channel.",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let request: SlackPostMessageRequest;
|
|
268
|
+
let actualRequest: SlackPostMessageRequest;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
request = buildSlackPostMessageRequest(input);
|
|
272
|
+
actualRequest = buildSlackPostMessageRequestInternal(input, {
|
|
273
|
+
redactToken: false,
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
retryable: false,
|
|
279
|
+
code: "channel_send_failed",
|
|
280
|
+
message: error instanceof Error ? error.message : String(error),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let response: Response;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
response = await fetcher(actualRequest.url, {
|
|
288
|
+
method: actualRequest.method,
|
|
289
|
+
headers: actualRequest.headers,
|
|
290
|
+
body: JSON.stringify(actualRequest.body),
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
retryable: true,
|
|
296
|
+
code: "channel_provider_unavailable",
|
|
297
|
+
message: `Slack chat.postMessage request failed before a response: ${redactSlackToken(
|
|
298
|
+
error instanceof Error ? error.message : String(error),
|
|
299
|
+
input.secrets.SLACK_BOT_TOKEN,
|
|
300
|
+
)}`,
|
|
301
|
+
providerRequestId: request.idempotencyKey,
|
|
302
|
+
providerMetadata: {
|
|
303
|
+
method: request.method,
|
|
304
|
+
url: request.url,
|
|
305
|
+
headers: request.headers,
|
|
306
|
+
body: request.body,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const payload = await response.json().catch(() => null);
|
|
312
|
+
const retryAfter = response.headers.get("retry-after") ?? undefined;
|
|
313
|
+
|
|
314
|
+
if (!response.ok || !isRecord(payload) || payload.ok !== true) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
318
|
+
code: response.status === 429 || response.status >= 500 ? "channel_provider_unavailable" : "channel_send_failed",
|
|
319
|
+
message: redactSlackToken(readSlackErrorMessage(payload, response.status), input.secrets.SLACK_BOT_TOKEN),
|
|
320
|
+
providerRequestId: request.idempotencyKey,
|
|
321
|
+
providerMetadata: {
|
|
322
|
+
method: request.method,
|
|
323
|
+
url: request.url,
|
|
324
|
+
status: response.status,
|
|
325
|
+
...(retryAfter ? { retryAfter } : {}),
|
|
326
|
+
headers: request.headers,
|
|
327
|
+
body: request.body,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const providerMessageId = readString(payload, "ts");
|
|
333
|
+
|
|
334
|
+
if (!providerMessageId) {
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
retryable: false,
|
|
338
|
+
code: "channel_send_failed",
|
|
339
|
+
message: "Slack chat.postMessage did not return ts.",
|
|
340
|
+
providerRequestId: request.idempotencyKey,
|
|
341
|
+
providerMetadata: {
|
|
342
|
+
method: request.method,
|
|
343
|
+
url: request.url,
|
|
344
|
+
status: response.status,
|
|
345
|
+
headers: request.headers,
|
|
346
|
+
body: request.body,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
status: "sent",
|
|
354
|
+
providerRequestId: request.idempotencyKey,
|
|
355
|
+
providerMessageId,
|
|
356
|
+
providerMetadata: {
|
|
357
|
+
method: request.method,
|
|
358
|
+
url: request.url,
|
|
359
|
+
status: response.status,
|
|
360
|
+
headers: request.headers,
|
|
361
|
+
body: request.body,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function verifySlackWebhook(input: {
|
|
367
|
+
headers: Headers;
|
|
368
|
+
rawBody: string;
|
|
369
|
+
secrets: Record<string, string>;
|
|
370
|
+
}): Promise<WebhookVerificationResult> {
|
|
371
|
+
const signingSecret = input.secrets.SLACK_SIGNING_SECRET;
|
|
372
|
+
|
|
373
|
+
if (!signingSecret) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
code: "channel_secret_missing",
|
|
377
|
+
message: "Secret SLACK_SIGNING_SECRET is not set for this channel.",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const signature = input.headers.get("x-slack-signature");
|
|
382
|
+
const timestamp = input.headers.get("x-slack-request-timestamp");
|
|
383
|
+
|
|
384
|
+
if (!signature || !timestamp) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
code: "channel_signature_invalid",
|
|
388
|
+
message: "Slack signature headers are missing.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const timestampSeconds = Number(timestamp);
|
|
393
|
+
|
|
394
|
+
if (!Number.isInteger(timestampSeconds)) {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
code: "channel_signature_invalid",
|
|
398
|
+
message: "Slack request timestamp is invalid.",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - timestampSeconds) > SLACK_REPLAY_WINDOW_SECONDS) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
code: "channel_signature_invalid",
|
|
406
|
+
message: "Slack request timestamp is outside the allowed replay window.",
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!signature.startsWith(`${SLACK_SIGNATURE_VERSION}=`)) {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
code: "channel_signature_invalid",
|
|
414
|
+
message: "Slack request signature version is unsupported.",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const signatureBytes = hexToBytes(signature.slice(`${SLACK_SIGNATURE_VERSION}=`.length));
|
|
419
|
+
|
|
420
|
+
if (!signatureBytes || signatureBytes.byteLength !== 32) {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
code: "channel_signature_invalid",
|
|
424
|
+
message: "Slack request signature is not valid hex.",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const encoder = new TextEncoder();
|
|
429
|
+
const key = await crypto.subtle.importKey(
|
|
430
|
+
"raw",
|
|
431
|
+
encoder.encode(signingSecret),
|
|
432
|
+
{
|
|
433
|
+
name: "HMAC",
|
|
434
|
+
hash: "SHA-256",
|
|
435
|
+
},
|
|
436
|
+
false,
|
|
437
|
+
["verify"],
|
|
438
|
+
);
|
|
439
|
+
const signedBody = encoder.encode(`${SLACK_SIGNATURE_VERSION}:${timestamp}:${input.rawBody}`);
|
|
440
|
+
const ok = await crypto.subtle.verify("HMAC", key, bytesToArrayBuffer(signatureBytes), bytesToArrayBuffer(signedBody));
|
|
441
|
+
|
|
442
|
+
if (!ok) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
code: "channel_signature_invalid",
|
|
446
|
+
message: "Slack request signature did not match.",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { ok: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildSlackPostMessageRequestInternal(
|
|
454
|
+
input: ChannelSendInput,
|
|
455
|
+
options: { redactToken: boolean },
|
|
456
|
+
): SlackPostMessageRequest {
|
|
457
|
+
if (input.replyTarget?.type !== "slack_message") {
|
|
458
|
+
throw new Error("Slack sendMessage requires a Slack message reply target.");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const token = options.redactToken ? "<redacted>" : input.secrets.SLACK_BOT_TOKEN;
|
|
462
|
+
|
|
463
|
+
if (!token) {
|
|
464
|
+
throw new AgentKitError("channel_secret_missing", "Secret SLACK_BOT_TOKEN is not set for this channel.");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const baseUrl = input.secrets.AGENTKIT_SLACK_API_BASE_URL ?? SLACK_API_BASE_URL;
|
|
468
|
+
const url = new URL("/api/chat.postMessage", baseUrl);
|
|
469
|
+
const target = input.replyTarget;
|
|
470
|
+
const threadTs = target.threadTs;
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
method: "POST",
|
|
474
|
+
url: url.href,
|
|
475
|
+
headers: {
|
|
476
|
+
Authorization: `Bearer ${token}`,
|
|
477
|
+
"Content-Type": "application/json",
|
|
478
|
+
},
|
|
479
|
+
body: {
|
|
480
|
+
channel: target.channelId,
|
|
481
|
+
text: sanitizeSlackOutboundText(input.message.content),
|
|
482
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
483
|
+
mrkdwn: true,
|
|
484
|
+
parse: "none",
|
|
485
|
+
link_names: false,
|
|
486
|
+
unfurl_links: false,
|
|
487
|
+
unfurl_media: false,
|
|
488
|
+
},
|
|
489
|
+
idempotencyKey: `slack:${input.channelId}:${input.conversationId}:${target.channelId}:${threadTs ?? target.messageTs}`,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function slackBaseEvent(
|
|
494
|
+
input: RawWebhookEvent,
|
|
495
|
+
options: {
|
|
496
|
+
providerEventId: string;
|
|
497
|
+
teamId: string;
|
|
498
|
+
channelId?: string;
|
|
499
|
+
userId?: string;
|
|
500
|
+
threadTs?: string;
|
|
501
|
+
},
|
|
502
|
+
): Omit<NormalizedChannelEvent, "kind"> {
|
|
503
|
+
const identity = options.channelId
|
|
504
|
+
? options.threadTs
|
|
505
|
+
? {
|
|
506
|
+
key: `slack:team:${options.teamId}:channel:${options.channelId}:thread:${options.threadTs}`,
|
|
507
|
+
display: `slack:team:${options.teamId}:channel:${options.channelId}:thread:${options.threadTs}`,
|
|
508
|
+
redactedDisplay: `slack:team:${redactSlackId(options.teamId)}:channel:${redactSlackId(
|
|
509
|
+
options.channelId,
|
|
510
|
+
)}:thread:${redactSlackTs(options.threadTs)}`,
|
|
511
|
+
}
|
|
512
|
+
: {
|
|
513
|
+
key: `slack:team:${options.teamId}:dm:${options.channelId}:user:${options.userId ?? "unknown"}`,
|
|
514
|
+
display: `slack:team:${options.teamId}:dm:${options.channelId}:user:${options.userId ?? "unknown"}`,
|
|
515
|
+
redactedDisplay: `slack:team:${redactSlackId(options.teamId)}:dm:${redactSlackId(
|
|
516
|
+
options.channelId,
|
|
517
|
+
)}:user:${redactSlackId(options.userId ?? "unknown")}`,
|
|
518
|
+
}
|
|
519
|
+
: {
|
|
520
|
+
key: `slack:team:${options.teamId}:unknown`,
|
|
521
|
+
redactedDisplay: `slack:team:${redactSlackId(options.teamId)}:unknown`,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
type: "slack",
|
|
526
|
+
provider: "slack",
|
|
527
|
+
providerEventId: options.providerEventId,
|
|
528
|
+
dedupeKey: `slack:${input.channelId}:${options.providerEventId}`,
|
|
529
|
+
externalIdentity: identity,
|
|
530
|
+
receivedAt: input.receivedAt,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function slackProviderMetadata(body: Record<string, unknown>, event: Record<string, unknown>): Record<string, unknown> {
|
|
535
|
+
return {
|
|
536
|
+
envelopeType: readString(body, "type"),
|
|
537
|
+
eventType: readString(event, "type"),
|
|
538
|
+
channelType: readString(event, "channel_type"),
|
|
539
|
+
teamId: readString(body, "team_id"),
|
|
540
|
+
apiAppId: readString(body, "api_app_id"),
|
|
541
|
+
eventTime: readNumber(body, "event_time"),
|
|
542
|
+
...(readString(event, "thread_ts") ? { threadTs: readString(event, "thread_ts") } : {}),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function parseJsonObject(input: { rawBody: string }): Record<string, unknown> {
|
|
547
|
+
try {
|
|
548
|
+
const parsed = JSON.parse(input.rawBody) as unknown;
|
|
549
|
+
|
|
550
|
+
if (!isRecord(parsed)) {
|
|
551
|
+
throw new Error("Slack payload must be a JSON object.");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return parsed;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
if (error instanceof AgentKitError) {
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
throw new AgentKitError("channel_payload_invalid", "Slack payload must be valid JSON.");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function readSlackErrorMessage(payload: unknown, status: number): string {
|
|
565
|
+
if (isRecord(payload)) {
|
|
566
|
+
const error = readString(payload, "error");
|
|
567
|
+
|
|
568
|
+
if (error) {
|
|
569
|
+
return `Slack chat.postMessage failed: ${error}.`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return `Slack chat.postMessage returned HTTP ${status}.`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function stripSlackBotMention(text: string): string {
|
|
577
|
+
return text.replace(/^(?:<@[A-Z0-9]+>\s*)+/, "").trim();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function sanitizeSlackOutboundText(text: string): string {
|
|
581
|
+
return text
|
|
582
|
+
.replace(/<@/g, "<@")
|
|
583
|
+
.replace(/<!channel/g, "<!channel")
|
|
584
|
+
.replace(/<!here/g, "<!here")
|
|
585
|
+
.replace(/<!everyone/g, "<!everyone")
|
|
586
|
+
.replace(/<!subteam/g, "<!subteam");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function redactSlackToken(value: string, token: string | undefined): string {
|
|
590
|
+
return token ? value.split(token).join("<redacted>") : value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function redactSlackId(value: string): string {
|
|
594
|
+
if (value.length <= 4) {
|
|
595
|
+
return "***";
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return `${value.slice(0, 2)}***${value.slice(-2)}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function redactSlackTs(value: string): string {
|
|
602
|
+
const parts = value.split(".");
|
|
603
|
+
|
|
604
|
+
if (parts.length !== 2) {
|
|
605
|
+
return redactSlackId(value);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return `${parts[0]}.***`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function readRecord(value: Record<string, unknown>, key: string): Record<string, unknown> {
|
|
612
|
+
const nested = value[key];
|
|
613
|
+
return isRecord(nested) ? nested : {};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function readString(value: Record<string, unknown>, key: string): string | undefined {
|
|
617
|
+
const raw = value[key];
|
|
618
|
+
return typeof raw === "string" ? raw : undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function readNumber(value: Record<string, unknown>, key: string): number | undefined {
|
|
622
|
+
const raw = value[key];
|
|
623
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
627
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function hexToBytes(value: string): Uint8Array | null {
|
|
631
|
+
if (!/^[a-fA-F0-9]+$/.test(value) || value.length % 2 !== 0) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
636
|
+
|
|
637
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
638
|
+
bytes[index / 2] = Number.parseInt(value.slice(index, index + 2), 16);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return bytes;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function bytesToArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
645
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
|
|
646
|
+
}
|