@andreprado/agentkit 0.1.0-alpha.9 → 0.1.1
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 +904 -0
- package/src/runtime/channels/generic-webhook.ts +682 -0
- package/src/runtime/channels/net-guard.ts +480 -0
- package/src/runtime/channels/provider-fetch.ts +54 -0
- package/src/runtime/channels/slack.ts +652 -0
- package/src/runtime/channels/telegram.ts +379 -15
- package/src/runtime/channels/whatsapp-evolution.ts +1330 -0
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1192 -0
- package/src/runtime/channels/whatsapp-zapster.ts +702 -40
- package/src/runtime/channels.ts +83 -3
- package/src/runtime/chat.ts +70 -44
- package/src/runtime/config.ts +512 -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/embeddings.ts +45 -7
- package/src/runtime/knowledge/ingest.ts +69 -6
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/knowledge/vector.ts +30 -30
- 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
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
522
|
-
const
|
|
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
|
|
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
|
-
|
|
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 {
|