@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.
Files changed (117) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +856 -36
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +67 -9
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +896 -0
  47. package/src/runtime/channels/generic-webhook.ts +974 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +343 -12
  50. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  51. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  52. package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
  53. package/src/runtime/channels/whatsapp-zapster.ts +674 -40
  54. package/src/runtime/channels.ts +83 -3
  55. package/src/runtime/chat.ts +70 -44
  56. package/src/runtime/config.ts +489 -20
  57. package/src/runtime/core/manifest.ts +75 -5
  58. package/src/runtime/core/targets.ts +5 -5
  59. package/src/runtime/deploy-readiness.ts +34 -4
  60. package/src/runtime/dev-server.ts +639 -39
  61. package/src/runtime/env.ts +8 -3
  62. package/src/runtime/evals.ts +445 -74
  63. package/src/runtime/improve.ts +868 -0
  64. package/src/runtime/inspect.ts +173 -4
  65. package/src/runtime/integrations/composio.ts +425 -0
  66. package/src/runtime/knowledge/retrieve.ts +25 -5
  67. package/src/runtime/knowledge/schema.ts +45 -1
  68. package/src/runtime/prompt-context.ts +141 -0
  69. package/src/runtime/runtime-contract.ts +71 -7
  70. package/src/runtime/skills.ts +95 -0
  71. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  72. package/src/runtime/targets/container/server.ts +1 -1
  73. package/src/runtime/targets/vps/deploy.ts +26 -9
  74. package/src/runtime/tool-runner.ts +9 -1
  75. package/src/runtime/tools.ts +26 -2
  76. package/src/runtime/transcription.ts +483 -0
  77. package/src/storage/sqlite.ts +7 -2
  78. package/src/templates/blank.ts +37 -9
  79. package/src/templates/dentista.ts +40 -14
  80. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  81. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  82. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  83. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  84. package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
  87. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  88. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  89. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  90. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  91. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  92. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  93. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  94. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  95. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  96. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  97. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  98. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  99. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  100. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  101. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  102. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  103. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  105. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  106. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  107. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  108. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  109. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  110. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  111. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  112. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  113. package/src/templates/support.ts +42 -12
  114. package/docs/guides/agentkit-skills-architecture.md +0 -471
  115. package/docs/guides/channels-implementation-map.md +0 -243
  116. package/docs/guides/channels-production-handoff.md +0 -101
  117. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -7,10 +7,15 @@ import { Readable } from "node:stream";
7
7
 
8
8
  import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
9
9
  import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
10
- import type { ChannelAdapter, RawWebhookEvent } from "./channels";
10
+ import type { ChannelAdapter, NormalizedChannelMessage, RawWebhookEvent } from "./channels";
11
+ import { discordChannelAdapter } from "./channels/discord";
12
+ import { genericWebhookChannelAdapter } from "./channels/generic-webhook";
13
+ import { slackChannelAdapter } from "./channels/slack";
11
14
  import { telegramChannelAdapter } from "./channels/telegram";
12
15
  import { websiteChannelAdapter } from "./channels/website";
16
+ import { evolutionWhatsappChannelAdapter } from "./channels/whatsapp-evolution";
13
17
  import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
18
+ import { uazapiWhatsappChannelAdapter } from "./channels/whatsapp-uazapi";
14
19
  import { zapsterWhatsappChannelAdapter } from "./channels/whatsapp-zapster";
15
20
  import { runAgentMessage } from "./chat";
16
21
  import { isChannelBufferEnabled, renderBufferedChannelMessage, resolveChannelBuffer } from "./channel-buffer";
@@ -20,11 +25,13 @@ import { loadCapsuleEnv } from "./env";
20
25
  import { buildInspectState } from "./inspect";
21
26
  import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
22
27
  import { runToolFromCwd } from "./tool-runner";
28
+ import { resolveTranscriptionConfig, transcribeAudio } from "./transcription";
23
29
 
24
30
  export type AgentDevServerOptions = {
25
31
  port?: number;
26
32
  hostname?: string;
27
33
  basePort?: number;
34
+ access?: AgentDevServerAccessPolicy;
28
35
  };
29
36
 
30
37
  export type AgentDevServer = {
@@ -48,9 +55,19 @@ export type AgentDevServer = {
48
55
 
49
56
  const DEFAULT_PORT = 4123;
50
57
  const DEFAULT_HOSTNAME = "localhost";
58
+ const MAX_JSON_BODY_BYTES = 1_048_576;
59
+ const MAX_WEBHOOK_BODY_BYTES = 1_048_576;
60
+ const CHANNEL_DEDUPE_TTL_MS = 6 * 60 * 60 * 1000;
61
+ const CHANNEL_DEDUPE_MAX_KEYS = 10_000;
62
+
63
+ export type AgentDevServerAccessPolicy = "local" | "enforce";
64
+
65
+ type DevHttpServerOptions = {
66
+ access: AgentDevServerAccessPolicy;
67
+ };
51
68
 
52
69
  type DevChannelRuntimeState = {
53
- dedupeKeys: Set<string>;
70
+ dedupeKeys: BoundedDedupeCache;
54
71
  buffers: Map<string, DevChannelBuffer>;
55
72
  };
56
73
 
@@ -74,7 +91,9 @@ export async function startAgentDevServer(
74
91
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
75
92
  const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
76
93
  const autoPort = options.port === undefined;
77
- const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
94
+ const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort, {
95
+ access: options.access ?? "local",
96
+ });
78
97
 
79
98
  if (start.actualPort === undefined) {
80
99
  throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
@@ -106,9 +125,10 @@ async function listenForDevServer(
106
125
  hostname: string,
107
126
  requestedPort: number,
108
127
  autoPort: boolean,
128
+ serverOptions: DevHttpServerOptions,
109
129
  ): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
110
130
  if (requestedPort === 0) {
111
- const server = createDevHttpServer(capsule);
131
+ const server = createDevHttpServer(capsule, serverOptions);
112
132
  const actualPort = await listen(server, hostname, await findAvailablePort());
113
133
  return { server, actualPort, portConflict: null };
114
134
  }
@@ -117,7 +137,7 @@ async function listenForDevServer(
117
137
  const lastPort = autoPort ? requestedPort + 50 : requestedPort;
118
138
 
119
139
  for (let port = requestedPort; port <= lastPort; port += 1) {
120
- const server = createDevHttpServer(capsule);
140
+ const server = createDevHttpServer(capsule, serverOptions);
121
141
 
122
142
  try {
123
143
  const actualPort = await listen(server, hostname, port);
@@ -142,14 +162,14 @@ async function listenForDevServer(
142
162
  );
143
163
  }
144
164
 
145
- function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
165
+ function createDevHttpServer(capsule: LoadedAgentCapsule, options: DevHttpServerOptions): Server {
146
166
  const channelRuntime: DevChannelRuntimeState = {
147
- dedupeKeys: new Set<string>(),
167
+ dedupeKeys: new BoundedDedupeCache(CHANNEL_DEDUPE_MAX_KEYS, CHANNEL_DEDUPE_TTL_MS),
148
168
  buffers: new Map<string, DevChannelBuffer>(),
149
169
  };
150
170
  const capsuleRoot = capsule.root;
151
171
  return createHttpServer((incoming, outgoing) => {
152
- void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
172
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime, options);
153
173
  });
154
174
  }
155
175
 
@@ -158,18 +178,19 @@ async function handleNodeRequest(
158
178
  incoming: IncomingMessage,
159
179
  outgoing: ServerResponse,
160
180
  channelRuntime: DevChannelRuntimeState,
181
+ options: DevHttpServerOptions,
161
182
  ): Promise<void> {
162
183
  try {
184
+ const request = nodeRequestToFetchRequest(incoming, options);
163
185
  const capsule = await loadAgentCapsule(capsuleRoot);
164
- const request = nodeRequestToFetchRequest(incoming);
165
- const response = await handleDevServerRequest(capsule, request, channelRuntime);
186
+ const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
166
187
  await writeFetchResponse(outgoing, response);
167
188
  } catch (error) {
168
189
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
169
190
  }
170
191
  }
171
192
 
172
- function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
193
+ function nodeRequestToFetchRequest(incoming: IncomingMessage, options: DevHttpServerOptions): Request {
173
194
  const headers = new Headers();
174
195
 
175
196
  for (const [key, value] of Object.entries(incoming.headers)) {
@@ -182,9 +203,24 @@ function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
182
203
  }
183
204
  }
184
205
 
185
- const protocol = headers.get("x-forwarded-proto") ?? "http";
186
206
  const host = headers.get("host") ?? `${DEFAULT_HOSTNAME}:${DEFAULT_PORT}`;
187
- const url = new URL(incoming.url ?? "/", `${protocol}://${host}`).href;
207
+ const hostname = requestHeaderHostname(`http://${host}`, "Host");
208
+ const pathname = normalizePath(new URL(incoming.url ?? "/", "http://localhost").pathname);
209
+
210
+ if (options.access === "local" && !isChannelWebhookPath(pathname)) {
211
+ assertLoopbackHostname(hostname, "Host");
212
+
213
+ const origin = headers.get("origin");
214
+ if (origin) {
215
+ assertLoopbackHostname(requestHeaderHostname(origin, "Origin"), "Origin");
216
+ }
217
+
218
+ if (headers.get("sec-fetch-site") === "cross-site") {
219
+ throw new AgentKitError("request_origin_invalid", "Cross-site browser requests are not allowed by the local dev server.");
220
+ }
221
+ }
222
+
223
+ const url = new URL(incoming.url ?? "/", `http://${host}`).href;
188
224
  const init: RequestInit & { duplex?: "half" } = {
189
225
  method: incoming.method ?? "GET",
190
226
  headers,
@@ -198,6 +234,28 @@ function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
198
234
  return new Request(url, init);
199
235
  }
200
236
 
237
+ function isChannelWebhookPath(pathname: string): boolean {
238
+ return /^\/channels\/[^/]+\/(?:(?:website|telegram|whatsapp|discord|slack|webhook)\/[^/]+\/)?webhook$/.test(
239
+ pathname,
240
+ );
241
+ }
242
+
243
+ function requestHeaderHostname(value: string, header: string): string {
244
+ try {
245
+ return new URL(value).hostname;
246
+ } catch {
247
+ throw new AgentKitError("request_origin_invalid", `${header} is not a valid URL authority.`);
248
+ }
249
+ }
250
+
251
+ function assertLoopbackHostname(hostname: string, header: string): void {
252
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]") {
253
+ return;
254
+ }
255
+
256
+ throw new AgentKitError("request_origin_invalid", `${header} must target localhost while dev access is local.`);
257
+ }
258
+
201
259
  async function writeFetchResponse(outgoing: ServerResponse, response: Response): Promise<void> {
202
260
  outgoing.statusCode = response.status;
203
261
  response.headers.forEach((value, key) => {
@@ -222,27 +280,43 @@ async function handleDevServerRequest(
222
280
  capsule: LoadedAgentCapsule,
223
281
  request: Request,
224
282
  channelRuntime: DevChannelRuntimeState,
283
+ options: DevHttpServerOptions,
225
284
  ): Promise<Response> {
226
285
  try {
227
286
  const url = new URL(request.url);
228
287
  const route = normalizePath(url.pathname);
229
288
 
289
+ if (request.method === "GET" && route === "/health") {
290
+ return jsonResponse({ status: "ok", agent: capsule.config.name });
291
+ }
292
+
230
293
  if (request.method === "GET" && route === "/") {
294
+ const auth = await authorizeDevRoute(capsule, request, options, "chat");
295
+ if (auth) {
296
+ return auth;
297
+ }
298
+
231
299
  const env = await loadCapsuleEnv(capsule.root);
232
300
  return htmlResponse(renderChatApp(capsule, url.origin, env));
233
301
  }
234
302
 
235
303
  if (request.method === "GET" && route === "/_agentkit") {
304
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
305
+ if (auth) {
306
+ return auth;
307
+ }
308
+
236
309
  const env = await loadCapsuleEnv(capsule.root);
237
310
  return jsonResponse(buildInspectState(capsule, env));
238
311
  }
239
312
 
240
- if (request.method === "GET" && route === "/health") {
241
- return jsonResponse({ status: "ok", agent: capsule.config.name });
242
- }
243
-
244
313
  if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
245
314
  const body = await readJsonBody(request);
315
+ const auth = await authorizeDevRoute(capsule, request, options, "chat", body);
316
+ if (auth) {
317
+ return auth;
318
+ }
319
+
246
320
  const { message, conversationId } = readChatRequest(body);
247
321
  const result = await runAgentMessage(capsule, {
248
322
  message,
@@ -262,6 +336,11 @@ async function handleDevServerRequest(
262
336
  const toolMatch = route.match(/^\/tools\/([^/]+)$/);
263
337
 
264
338
  if (request.method === "POST" && toolMatch) {
339
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
340
+ if (auth) {
341
+ return auth;
342
+ }
343
+
265
344
  const body = await readJsonBody(request);
266
345
  const result = await runToolFromCwd(capsule.root, {
267
346
  name: decodeURIComponent(toolMatch[1]),
@@ -275,32 +354,53 @@ async function handleDevServerRequest(
275
354
  const fileMatch = route.match(/^\/files\/(.+)$/);
276
355
 
277
356
  if (fileMatch) {
357
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
358
+ if (auth) {
359
+ return auth;
360
+ }
361
+
278
362
  return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
279
363
  }
280
364
 
281
- const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
365
+ const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp|discord|slack|webhook)\/([^/]+)\/webhook$/);
366
+ const directWebhookMatch = route.match(/^\/channels\/([^/]+)\/webhook$/);
282
367
 
283
- if (channelMatch) {
368
+ if (channelMatch || directWebhookMatch) {
284
369
  return await handlePortableChannel(capsule, request, channelRuntime, {
285
- name: decodeURIComponent(channelMatch[1]),
286
- type: channelMatch[2] as ChannelType,
287
- provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
370
+ name: decodeURIComponent((channelMatch ?? directWebhookMatch)?.[1] ?? ""),
371
+ type: channelMatch ? channelMatch[2] as ChannelType : "webhook",
372
+ provider: channelMatch ? decodeURIComponent(channelMatch[3]) as ChannelProvider : "generic",
288
373
  });
289
374
  }
290
375
 
291
376
  if (request.method === "GET" && route === "/v1/conversations") {
377
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
378
+ if (auth) {
379
+ return auth;
380
+ }
381
+
292
382
  return jsonResponse({ conversations: await listConversations(capsule) });
293
383
  }
294
384
 
295
385
  const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
296
386
 
297
387
  if (request.method === "GET" && conversationMatch) {
388
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
389
+ if (auth) {
390
+ return auth;
391
+ }
392
+
298
393
  return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
299
394
  }
300
395
 
301
396
  const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
302
397
 
303
398
  if (request.method === "GET" && conversationTraceMatch) {
399
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
400
+ if (auth) {
401
+ return auth;
402
+ }
403
+
304
404
  return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
305
405
  }
306
406
 
@@ -310,6 +410,109 @@ async function handleDevServerRequest(
310
410
  }
311
411
  }
312
412
 
413
+ async function authorizeDevRoute(
414
+ capsule: LoadedAgentCapsule,
415
+ request: Request,
416
+ options: DevHttpServerOptions,
417
+ kind: "chat" | "admin",
418
+ body?: unknown,
419
+ ): Promise<Response | null> {
420
+ if (options.access === "local") {
421
+ return null;
422
+ }
423
+
424
+ const env = await loadCapsuleEnv(capsule.root);
425
+
426
+ if (kind === "admin") {
427
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
428
+ missingCode: "deploy_not_ready",
429
+ missingMessage: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set.",
430
+ unauthorizedMessage: "Missing or invalid AgentKit admin token.",
431
+ });
432
+ }
433
+
434
+ if (capsule.config.access.mode === "public") {
435
+ return null;
436
+ }
437
+
438
+ if (capsule.config.access.mode === "private") {
439
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
440
+ missingCode: "deploy_not_ready",
441
+ missingMessage: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set.",
442
+ unauthorizedMessage: "Missing or invalid deploy access token.",
443
+ });
444
+ }
445
+
446
+ return authorizeBearerSecret(request, body, env.AGENTKIT_ACCESS_TOKEN, {
447
+ missingCode: "deploy_not_ready",
448
+ missingMessage: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set.",
449
+ unauthorizedMessage: "Missing or invalid deploy access token.",
450
+ });
451
+ }
452
+
453
+ function authorizeBearerSecret(
454
+ request: Request,
455
+ body: unknown,
456
+ secret: string | undefined,
457
+ messages: { missingCode: string; missingMessage: string; unauthorizedMessage: string },
458
+ ): Response | null {
459
+ if (!secret) {
460
+ return jsonResponse(
461
+ {
462
+ error: {
463
+ code: messages.missingCode,
464
+ message: messages.missingMessage,
465
+ },
466
+ },
467
+ { status: 503 },
468
+ );
469
+ }
470
+
471
+ if (readRequestAccessToken(request, body) !== secret) {
472
+ return jsonResponse(
473
+ {
474
+ error: {
475
+ code: "unauthorized",
476
+ message: messages.unauthorizedMessage,
477
+ },
478
+ },
479
+ { status: 401 },
480
+ );
481
+ }
482
+
483
+ return null;
484
+ }
485
+
486
+ function readRequestAccessToken(request: Request, body: unknown): string {
487
+ const authorization = request.headers.get("authorization");
488
+
489
+ if (authorization?.startsWith("Bearer ")) {
490
+ return authorization.slice("Bearer ".length).trim();
491
+ }
492
+
493
+ const header =
494
+ request.headers.get("x-agentkit-access-token") ??
495
+ request.headers.get("x-agentkit-deploy-access-token") ??
496
+ "";
497
+ const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
498
+ if (token.trim()) {
499
+ return token.trim();
500
+ }
501
+
502
+ if (body && typeof body === "object") {
503
+ const record = body as { access_token?: unknown; accessToken?: unknown };
504
+ const bodyToken =
505
+ typeof record.access_token === "string"
506
+ ? record.access_token
507
+ : typeof record.accessToken === "string"
508
+ ? record.accessToken
509
+ : "";
510
+ return bodyToken.trim();
511
+ }
512
+
513
+ return "";
514
+ }
515
+
313
516
  async function handlePortableChannel(
314
517
  capsule: LoadedAgentCapsule,
315
518
  request: Request,
@@ -325,7 +528,7 @@ async function handlePortableChannel(
325
528
  }
326
529
 
327
530
  const adapter = adapterForChannel(route.type, route.provider);
328
- const rawBody = request.method === "GET" ? "" : await request.text();
531
+ const rawBody = request.method === "GET" ? "" : await readRequestTextWithLimit(request, MAX_WEBHOOK_BODY_BYTES);
329
532
  const rawEvent: RawWebhookEvent = {
330
533
  type: route.type,
331
534
  provider: route.provider,
@@ -345,8 +548,12 @@ async function handlePortableChannel(
345
548
  throw new AgentKitError(verification.code, verification.message);
346
549
  }
347
550
 
348
- if (channel.type === "whatsapp" && channel.provider === "meta" && request.method === "GET") {
349
- return new Response(new URL(request.url).searchParams.get("hub.challenge") ?? "");
551
+ const webhookResponse = adapter.getWebhookResponse
552
+ ? await adapter.getWebhookResponse({ ...rawEvent, channel, secrets })
553
+ : null;
554
+
555
+ if (webhookResponse) {
556
+ return channelAdapterResponse(webhookResponse);
350
557
  }
351
558
 
352
559
  const events = await adapter.normalizeInbound(rawEvent);
@@ -360,7 +567,9 @@ async function handlePortableChannel(
360
567
 
361
568
  channelRuntime.dedupeKeys.add(event.dedupeKey);
362
569
 
363
- if (event.kind !== "message" || !event.message) {
570
+ const processedMessage = await processDevChannelMessage(capsule, adapter, channel, event.message, secrets);
571
+
572
+ if (event.kind !== "message" || !processedMessage) {
364
573
  deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
365
574
  continue;
366
575
  }
@@ -376,7 +585,7 @@ async function handlePortableChannel(
376
585
  bufferDevChannelMessage(capsule, channelRuntime, {
377
586
  bufferKey: `${channel.name}:${event.externalIdentity.key}`,
378
587
  conversationId,
379
- content: event.message.content,
588
+ content: processedMessage.content,
380
589
  receivedAt: event.receivedAt,
381
590
  buffer,
382
591
  });
@@ -390,21 +599,214 @@ async function handlePortableChannel(
390
599
  }
391
600
 
392
601
  const result = await runAgentMessage(capsule, {
393
- message: event.message.content,
602
+ message: processedMessage.content,
394
603
  conversationId,
395
604
  signal: request.signal,
396
605
  });
606
+ const routedReply = channel.replyTo
607
+ ? await sendDevChannelReply({
608
+ capsule,
609
+ sourceChannel: channel,
610
+ sourceEvent: event,
611
+ rawBody,
612
+ conversationId: result.conversationId,
613
+ message: result.message.content,
614
+ })
615
+ : null;
616
+
397
617
  deliveries.push({
398
618
  state: "sent",
399
619
  dedupeKey: event.dedupeKey,
400
620
  conversationId: result.conversationId,
401
621
  message: result.message,
622
+ ...(routedReply ? { outbound: routedReply } : {}),
402
623
  });
403
624
  }
404
625
 
626
+ if (channel.type === "discord" && channel.provider === "discord") {
627
+ return jsonResponse(discordLocalInteractionResponse(deliveries));
628
+ }
629
+
405
630
  return jsonResponse({ deliveries });
406
631
  }
407
632
 
633
+ async function sendDevChannelReply(input: {
634
+ capsule: LoadedAgentCapsule;
635
+ sourceChannel: AgentChannel;
636
+ sourceEvent: Awaited<ReturnType<ChannelAdapter["normalizeInbound"]>>[number];
637
+ rawBody: string;
638
+ conversationId: string;
639
+ message: string;
640
+ }) {
641
+ const replyTo = input.sourceChannel.replyTo;
642
+
643
+ if (!replyTo) {
644
+ return null;
645
+ }
646
+
647
+ const target = (input.capsule.config.channels ?? []).find((channel) => channel.name === replyTo.channel);
648
+
649
+ if (!target) {
650
+ throw new AgentKitError("channel_not_found", `Reply target channel "${replyTo.channel}" was not found.`);
651
+ }
652
+
653
+ if (target.type === "webhook" && target.direction !== "outbound") {
654
+ throw new AgentKitError("channel_payload_invalid", `Reply target channel "${replyTo.channel}" is inbound-only.`);
655
+ }
656
+
657
+ const recipient = replyTo.recipientFrom ? readDevJsonPathScalar(input.rawBody, replyTo.recipientFrom) : null;
658
+
659
+ if (replyTo.recipientFrom && !recipient) {
660
+ throw new AgentKitError("channel_payload_invalid", `Reply route recipientFrom "${replyTo.recipientFrom}" did not resolve to a non-empty value.`);
661
+ }
662
+
663
+ const externalIdentity = recipient
664
+ ? recipientIdentityForDevChannel(target, recipient)
665
+ : input.sourceEvent.externalIdentity.key;
666
+
667
+ const targetAdapter = adapterForChannel(target.type, target.provider);
668
+ const secrets = await resolveChannelSecrets(input.capsule.root, target);
669
+
670
+ return targetAdapter.sendMessage({
671
+ type: target.type,
672
+ provider: target.provider,
673
+ channelId: target.name,
674
+ channel: target,
675
+ conversationId: input.conversationId,
676
+ externalIdentity: {
677
+ key: externalIdentity,
678
+ },
679
+ message: {
680
+ role: "assistant",
681
+ contentType: "text",
682
+ content: input.message,
683
+ },
684
+ secrets,
685
+ });
686
+ }
687
+
688
+ function recipientIdentityForDevChannel(channel: AgentChannel, recipient: string): string {
689
+ if (channel.type === "whatsapp") {
690
+ const prefix = `whatsapp:${channel.provider}:contact:`;
691
+ return recipient.startsWith(prefix) ? recipient : `${prefix}${recipient}`;
692
+ }
693
+
694
+ if (channel.type === "telegram") {
695
+ return recipient.includes(":chat:") ? recipient : `telegram:reply:chat:${recipient}`;
696
+ }
697
+
698
+ if (channel.type === "webhook" && channel.direction === "outbound") {
699
+ return recipient.startsWith("webhook:generic:") ? recipient : `webhook:generic:recipient:${recipient}`;
700
+ }
701
+
702
+ if (channel.type === "website") {
703
+ return recipient.startsWith("website:") ? recipient : `website:reply:${createHash("sha256").update(recipient).digest("hex").slice(0, 32)}`;
704
+ }
705
+
706
+ throw new AgentKitError(
707
+ "channel_payload_invalid",
708
+ `Reply target ${channel.type}/${channel.provider} requires a provider-native reply target and cannot use recipientFrom.`,
709
+ );
710
+ }
711
+
712
+ function readDevJsonPathScalar(rawBody: string, path: string): string | undefined {
713
+ let value: unknown;
714
+
715
+ try {
716
+ value = JSON.parse(rawBody) as unknown;
717
+ } catch {
718
+ return undefined;
719
+ }
720
+
721
+ for (const segment of path.replace(/^payload\./, "").split(".")) {
722
+ if (!isRecord(value)) {
723
+ return undefined;
724
+ }
725
+
726
+ value = value[segment];
727
+ }
728
+
729
+ if (typeof value === "string") {
730
+ const trimmed = value.trim();
731
+ return trimmed.length > 0 ? trimmed : undefined;
732
+ }
733
+
734
+ if (typeof value === "number" && Number.isFinite(value)) {
735
+ return String(value);
736
+ }
737
+
738
+ if (typeof value === "boolean") {
739
+ return String(value);
740
+ }
741
+
742
+ return undefined;
743
+ }
744
+
745
+ async function processDevChannelMessage(
746
+ capsule: LoadedAgentCapsule,
747
+ adapter: ChannelAdapter,
748
+ channel: AgentChannel,
749
+ message: NormalizedChannelMessage | undefined,
750
+ secrets: Record<string, string>,
751
+ ): Promise<NormalizedChannelMessage | null> {
752
+ if (!message) {
753
+ return null;
754
+ }
755
+
756
+ if (message.contentType === "text" && message.content) {
757
+ return message;
758
+ }
759
+
760
+ if (message.contentType !== "audio" || !message.audio || channel.audio?.mode !== "transcribe") {
761
+ return null;
762
+ }
763
+
764
+ if (!adapter.downloadMedia) {
765
+ return null;
766
+ }
767
+
768
+ const env = await loadCapsuleEnv(capsule.root);
769
+ const transcriptionConfig = resolveTranscriptionConfig(capsule.config.transcription);
770
+ const secretName = transcriptionConfig.secret;
771
+ const download = await adapter.downloadMedia({
772
+ type: channel.type,
773
+ provider: channel.provider,
774
+ channelId: channel.name,
775
+ message,
776
+ secrets: {
777
+ ...secrets,
778
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
779
+ ...(env.AGENTKIT_TELEGRAM_API_BASE_URL ? { AGENTKIT_TELEGRAM_API_BASE_URL: env.AGENTKIT_TELEGRAM_API_BASE_URL } : {}),
780
+ },
781
+ });
782
+
783
+ if (!download.ok) {
784
+ return null;
785
+ }
786
+
787
+ const transcription = await transcribeAudio({
788
+ config: transcriptionConfig,
789
+ secrets: {
790
+ ...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
791
+ },
792
+ audio: download.audio,
793
+ filename: download.filename,
794
+ ...(download.mimeType ? { mimeType: download.mimeType } : {}),
795
+ ...(download.durationSeconds !== undefined ? { durationSeconds: download.durationSeconds } : {}),
796
+ });
797
+
798
+ if (!transcription.ok) {
799
+ return null;
800
+ }
801
+
802
+ return {
803
+ role: "user",
804
+ contentType: "text",
805
+ content: `The user sent an audio message.\n\nTranscript:\n${transcription.text}`,
806
+ providerMessageId: message.providerMessageId,
807
+ };
808
+ }
809
+
408
810
  function bufferDevChannelMessage(
409
811
  capsule: LoadedAgentCapsule,
410
812
  runtime: DevChannelRuntimeState,
@@ -509,6 +911,10 @@ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
509
911
  }
510
912
 
511
913
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
914
+ if (capsule.config.storage.files?.driver !== "r2") {
915
+ throw new AgentKitError("files_not_configured", "File storage is not configured for this Agent Capsule.");
916
+ }
917
+
512
918
  const key = normalizeFileKey(rawKey);
513
919
  const root = resolve(capsule.root, ".agentkit/files");
514
920
  const path = resolve(root, key);
@@ -518,14 +924,8 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
518
924
  }
519
925
 
520
926
  if (request.method === "PUT") {
521
- const bytes = new Uint8Array(await request.arrayBuffer());
522
- const maxUploadBytes = capsule.config.storage.files?.driver === "r2"
523
- ? capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024
524
- : 10 * 1024 * 1024;
525
-
526
- if (bytes.byteLength > maxUploadBytes) {
527
- throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
528
- }
927
+ const maxUploadBytes = capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024;
928
+ const bytes = await readRequestBytesWithLimit(request, maxUploadBytes);
529
929
 
530
930
  await mkdir(dirname(path), { recursive: true });
531
931
  await writeFile(path, bytes);
@@ -548,6 +948,112 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
548
948
  throw new AgentKitError("method_not_allowed", "Unsupported file method.");
549
949
  }
550
950
 
951
+ export class BoundedDedupeCache {
952
+ private readonly entries = new Map<string, number>();
953
+
954
+ constructor(
955
+ private readonly maxKeys: number,
956
+ private readonly ttlMs: number,
957
+ ) {}
958
+
959
+ has(key: string): boolean {
960
+ this.pruneExpired();
961
+ return this.entries.has(key);
962
+ }
963
+
964
+ add(key: string): void {
965
+ this.pruneExpired();
966
+ this.entries.set(key, Date.now());
967
+ this.pruneOverflow();
968
+ }
969
+
970
+ get size(): number {
971
+ this.pruneExpired();
972
+ return this.entries.size;
973
+ }
974
+
975
+ private pruneExpired(): void {
976
+ const cutoff = Date.now() - this.ttlMs;
977
+
978
+ for (const [key, timestamp] of this.entries) {
979
+ if (timestamp >= cutoff) {
980
+ continue;
981
+ }
982
+
983
+ this.entries.delete(key);
984
+ }
985
+ }
986
+
987
+ private pruneOverflow(): void {
988
+ while (this.entries.size > this.maxKeys) {
989
+ const oldest = this.entries.keys().next().value;
990
+
991
+ if (typeof oldest !== "string") {
992
+ return;
993
+ }
994
+
995
+ this.entries.delete(oldest);
996
+ }
997
+ }
998
+ }
999
+
1000
+ async function readRequestBytesWithLimit(
1001
+ request: Request,
1002
+ maxBytes: number,
1003
+ errorCode = "file_too_large",
1004
+ label = "File",
1005
+ ): Promise<Uint8Array> {
1006
+ const contentLength = request.headers.get("content-length");
1007
+
1008
+ if (contentLength) {
1009
+ const declaredBytes = Number(contentLength);
1010
+
1011
+ if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
1012
+ throw new AgentKitError(errorCode, `${label} exceeds the maximum size of ${maxBytes} bytes.`);
1013
+ }
1014
+ }
1015
+
1016
+ if (!request.body) {
1017
+ return new Uint8Array();
1018
+ }
1019
+
1020
+ const reader = request.body.getReader();
1021
+ const chunks: Uint8Array[] = [];
1022
+ let total = 0;
1023
+
1024
+ while (true) {
1025
+ const { done, value } = await reader.read();
1026
+
1027
+ if (done) {
1028
+ break;
1029
+ }
1030
+
1031
+ total += value.byteLength;
1032
+
1033
+ if (total > maxBytes) {
1034
+ await reader.cancel().catch(() => undefined);
1035
+ throw new AgentKitError(errorCode, `${label} exceeds the maximum size of ${maxBytes} bytes.`);
1036
+ }
1037
+
1038
+ chunks.push(value);
1039
+ }
1040
+
1041
+ const bytes = new Uint8Array(total);
1042
+ let offset = 0;
1043
+
1044
+ for (const chunk of chunks) {
1045
+ bytes.set(chunk, offset);
1046
+ offset += chunk.byteLength;
1047
+ }
1048
+
1049
+ return bytes;
1050
+ }
1051
+
1052
+ async function readRequestTextWithLimit(request: Request, maxBytes: number): Promise<string> {
1053
+ const bytes = await readRequestBytesWithLimit(request, maxBytes, "request_too_large", "Request body");
1054
+ return new TextDecoder().decode(bytes);
1055
+ }
1056
+
551
1057
  async function listConversations(capsule: LoadedAgentCapsule) {
552
1058
  const store = await openCapsuleStore(capsule);
553
1059
 
@@ -612,8 +1118,12 @@ async function readJsonBody(request: Request): Promise<unknown> {
612
1118
  }
613
1119
 
614
1120
  try {
615
- return await request.json();
1121
+ return JSON.parse(await readRequestTextWithLimit(request, MAX_JSON_BODY_BYTES));
616
1122
  } catch (error) {
1123
+ if (isAgentKitError(error)) {
1124
+ throw error;
1125
+ }
1126
+
617
1127
  throw new AgentKitError("validation_error", "Request body must be valid JSON.", { cause: error });
618
1128
  }
619
1129
  }
@@ -701,11 +1211,22 @@ function statusForError(error: unknown): number {
701
1211
  return 409;
702
1212
  }
703
1213
 
1214
+ if (error.code === "request_origin_invalid" || error.code === "tool_authorization_required") {
1215
+ return 403;
1216
+ }
1217
+
1218
+ if (error.code === "request_too_large") {
1219
+ return 413;
1220
+ }
1221
+
1222
+ if (error.code === "channel_signature_invalid") {
1223
+ return 401;
1224
+ }
1225
+
704
1226
  if (
705
1227
  error.code === "file_key_invalid" ||
706
1228
  error.code === "file_too_large" ||
707
1229
  error.code === "channel_payload_invalid" ||
708
- error.code === "channel_signature_invalid" ||
709
1230
  error.code === "channel_secret_missing"
710
1231
  ) {
711
1232
  return 400;
@@ -715,6 +1236,7 @@ function statusForError(error: unknown): number {
715
1236
  error.code === "conversation_not_found" ||
716
1237
  error.code === "not_found" ||
717
1238
  error.code === "tool_not_found" ||
1239
+ error.code === "files_not_configured" ||
718
1240
  error.code === "file_not_found" ||
719
1241
  error.code === "channel_not_found"
720
1242
  ) {
@@ -807,12 +1329,90 @@ function adapterForChannel(type: ChannelType, provider: ChannelProvider): Channe
807
1329
  return metaWhatsappChannelAdapter;
808
1330
  }
809
1331
 
1332
+ if (type === "whatsapp" && provider === "uazapi") {
1333
+ return uazapiWhatsappChannelAdapter;
1334
+ }
1335
+
1336
+ if (type === "whatsapp" && provider === "evolution") {
1337
+ return evolutionWhatsappChannelAdapter;
1338
+ }
1339
+
1340
+ if (type === "discord" && provider === "discord") {
1341
+ return discordChannelAdapter;
1342
+ }
1343
+
1344
+ if (type === "slack" && provider === "slack") {
1345
+ return slackChannelAdapter;
1346
+ }
1347
+
1348
+ if (type === "webhook" && provider === "generic") {
1349
+ return genericWebhookChannelAdapter;
1350
+ }
1351
+
810
1352
  throw new AgentKitError("channel_not_found", `No channel adapter for ${type}/${provider}.`);
811
1353
  }
812
1354
 
1355
+ function channelAdapterResponse(response: {
1356
+ status?: number;
1357
+ headers?: Record<string, string>;
1358
+ body: string | Record<string, unknown>;
1359
+ }): Response {
1360
+ if (typeof response.body === "string") {
1361
+ return new Response(response.body, {
1362
+ status: response.status ?? 200,
1363
+ headers: response.headers,
1364
+ });
1365
+ }
1366
+
1367
+ return jsonResponse(response.body, {
1368
+ status: response.status ?? 200,
1369
+ headers: response.headers,
1370
+ });
1371
+ }
1372
+
1373
+ function discordLocalInteractionResponse(
1374
+ deliveries: Array<{
1375
+ state: string;
1376
+ message?: {
1377
+ content?: string;
1378
+ };
1379
+ }>,
1380
+ ): Record<string, unknown> {
1381
+ const sent = deliveries.find((delivery) => delivery.state === "sent" && delivery.message?.content);
1382
+
1383
+ if (sent?.message?.content) {
1384
+ return {
1385
+ type: 4,
1386
+ data: {
1387
+ content: sent.message.content,
1388
+ allowed_mentions: {
1389
+ parse: [],
1390
+ },
1391
+ },
1392
+ };
1393
+ }
1394
+
1395
+ return {
1396
+ type: 4,
1397
+ data: {
1398
+ content: "This Discord interaction was received but could not be processed.",
1399
+ flags: 64,
1400
+ allowed_mentions: {
1401
+ parse: [],
1402
+ },
1403
+ },
1404
+ };
1405
+ }
1406
+
813
1407
  async function resolveChannelSecrets(root: string, channel: AgentChannel): Promise<Record<string, string>> {
814
1408
  const env = await loadCapsuleEnv(root);
815
- return Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
1409
+ const secrets = Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
1410
+
1411
+ if (env.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
1412
+ secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN = "1";
1413
+ }
1414
+
1415
+ return secrets;
816
1416
  }
817
1417
 
818
1418
  function relativePath(root: string, path: string): string {