@andreprado/agentkit 0.1.0-alpha.24 → 0.1.0-alpha.26
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 +4 -1
- package/docs/guides/add-managed-composio.md +2 -2
- package/docs/guides/add-tool.md +4 -0
- package/docs/guides/channel-security.md +2 -0
- package/docs/guides/connect-whatsapp-uazapi.md +32 -19
- package/docs/guides/connect-whatsapp-zapster.md +35 -20
- package/docs/guides/create-agent.md +12 -0
- package/docs/guides/prepare-deploy.md +1 -1
- package/docs/guides/security-rules.md +3 -0
- package/docs/guides/use-jev.md +67 -0
- package/docs/llms-full.txt +17 -5
- package/docs/llms.txt +8 -2
- package/package.json +3 -4
- package/src/cli/commands/channels.ts +2 -2
- package/src/cli/help.ts +5 -3
- package/src/cli/index.ts +2 -5
- package/src/cli/new-command.ts +41 -0
- package/src/index.ts +2 -2
- package/src/runtime/channels/whatsapp-uazapi.ts +8 -12
- package/src/runtime/channels/whatsapp-zapster.ts +11 -14
- package/src/runtime/config.ts +2 -2
- package/src/runtime/dev-server.ts +74 -10
- package/src/runtime/integrations/composio.ts +3 -1
- package/src/runtime/targets/cloudflare/build.ts +20 -2
- package/src/runtime/tools.ts +18 -0
- package/src/templates/skills/agentkit-build-agent/SKILL.md +2 -2
- package/src/templates/skills/agentkit-capsule/SKILL.md +2 -2
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +5 -12
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +6 -12
- package/src/templates/skills/agentkit-integrations/SKILL.md +1 -1
- package/src/templates/skills/agentkit-security/SKILL.md +2 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +5 -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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
const newUsage = "agentkit new [name] [--template blank|support|dentista] [--no-install]";
|
|
4
|
+
|
|
5
|
+
export async function resolveNewProjectName(
|
|
6
|
+
positional: string[],
|
|
7
|
+
promptProjectName: () => Promise<string> = promptForProjectName,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
const [name] = positional;
|
|
10
|
+
|
|
11
|
+
if (name) {
|
|
12
|
+
return name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const promptedName = (await promptProjectName()).trim();
|
|
16
|
+
|
|
17
|
+
if (!promptedName) {
|
|
18
|
+
throw new Error(`Project name is required. Usage: ${newUsage}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return promptedName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function promptForProjectName(): Promise<string> {
|
|
25
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Missing project name. Usage: ${newUsage}. Run \`agentkit new\` in an interactive terminal to be prompted, or pass \`agentkit new <name>\` / \`agentkit new .\` in scripts.`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const readline = createInterface({
|
|
32
|
+
input: process.stdin,
|
|
33
|
+
output: process.stdout,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return await readline.question("Project folder name (or . for current directory): ");
|
|
38
|
+
} finally {
|
|
39
|
+
readline.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -627,11 +627,11 @@ function defaultDiscordChannelSecrets(mode: DiscordChannelMode): string[] {
|
|
|
627
627
|
|
|
628
628
|
function defaultWhatsappChannelSecrets(provider: WhatsappChannelProvider): string[] {
|
|
629
629
|
if (provider === "zapster") {
|
|
630
|
-
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
|
|
630
|
+
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"];
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
if (provider === "uazapi") {
|
|
634
|
-
return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"];
|
|
634
|
+
return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"];
|
|
635
635
|
}
|
|
636
636
|
|
|
637
637
|
if (provider === "evolution") {
|
|
@@ -55,12 +55,12 @@ const UAZAPI_IGNORED_EVENTS = new Set(["connection", "presence", "history"]);
|
|
|
55
55
|
export const uazapiWhatsappChannelAdapter: ChannelAdapter = {
|
|
56
56
|
type: "whatsapp",
|
|
57
57
|
provider: "uazapi",
|
|
58
|
-
requiredSecrets: ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"],
|
|
58
|
+
requiredSecrets: ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"],
|
|
59
59
|
verifyWebhook(input) {
|
|
60
|
-
const
|
|
60
|
+
const tokenResult = verifyWebhookToken(input);
|
|
61
61
|
|
|
62
|
-
if (!
|
|
63
|
-
return
|
|
62
|
+
if (!tokenResult.ok) {
|
|
63
|
+
return tokenResult;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
try {
|
|
@@ -513,7 +513,9 @@ export async function getUazapiStatus(
|
|
|
513
513
|
input: ChannelStatusInput,
|
|
514
514
|
fetcher: UazapiFetch = fetch,
|
|
515
515
|
): Promise<ChannelStatusResult> {
|
|
516
|
-
const missingSecrets = ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"].filter(
|
|
516
|
+
const missingSecrets = ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"].filter(
|
|
517
|
+
(secret) => !input.secrets[secret],
|
|
518
|
+
);
|
|
517
519
|
|
|
518
520
|
if (missingSecrets.length > 0) {
|
|
519
521
|
return {
|
|
@@ -617,13 +619,7 @@ export function uazapiApiUrl(baseUrl: string, path: string): string {
|
|
|
617
619
|
return new URL(normalizedPath, `${base}/`).href;
|
|
618
620
|
}
|
|
619
621
|
|
|
620
|
-
function
|
|
621
|
-
const expectsToken = input.channel.secrets.includes("UAZAPI_WEBHOOK_TOKEN");
|
|
622
|
-
|
|
623
|
-
if (!expectsToken) {
|
|
624
|
-
return { ok: true };
|
|
625
|
-
}
|
|
626
|
-
|
|
622
|
+
function verifyWebhookToken(input: WebhookVerificationInput): WebhookVerificationResult {
|
|
627
623
|
const expectedToken = input.secrets.UAZAPI_WEBHOOK_TOKEN;
|
|
628
624
|
|
|
629
625
|
if (!expectedToken) {
|
|
@@ -13,7 +13,7 @@ import { AgentKitError } from "../errors";
|
|
|
13
13
|
export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
14
14
|
type: "whatsapp",
|
|
15
15
|
provider: "zapster",
|
|
16
|
-
requiredSecrets: ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"],
|
|
16
|
+
requiredSecrets: ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"],
|
|
17
17
|
verifyWebhook(input) {
|
|
18
18
|
const expectedInstanceId = input.secrets.ZAPSTER_INSTANCE_ID;
|
|
19
19
|
const expectedWebhookId = input.secrets.ZAPSTER_WEBHOOK_ID;
|
|
@@ -34,10 +34,10 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const tokenResult = verifyWebhookToken(input);
|
|
38
38
|
|
|
39
|
-
if (!
|
|
40
|
-
return
|
|
39
|
+
if (!tokenResult.ok) {
|
|
40
|
+
return tokenResult;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const instanceId = input.headers.get("x-instance-id");
|
|
@@ -165,9 +165,12 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
165
165
|
return sendZapsterMessage(input);
|
|
166
166
|
},
|
|
167
167
|
getStatus(input) {
|
|
168
|
-
const missingSecrets = [
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const missingSecrets = [
|
|
169
|
+
"ZAPSTER_API_KEY",
|
|
170
|
+
"ZAPSTER_INSTANCE_ID",
|
|
171
|
+
"ZAPSTER_WEBHOOK_ID",
|
|
172
|
+
"ZAPSTER_WEBHOOK_TOKEN",
|
|
173
|
+
].filter((secret) => !input.secrets[secret]);
|
|
171
174
|
|
|
172
175
|
if (missingSecrets.length > 0) {
|
|
173
176
|
return {
|
|
@@ -260,13 +263,7 @@ function normalizeZapsterAudio(value: Record<string, unknown>): NormalizedZapste
|
|
|
260
263
|
};
|
|
261
264
|
}
|
|
262
265
|
|
|
263
|
-
function
|
|
264
|
-
const expectsToken = input.channel.secrets.includes("ZAPSTER_WEBHOOK_TOKEN");
|
|
265
|
-
|
|
266
|
-
if (!expectsToken) {
|
|
267
|
-
return { ok: true };
|
|
268
|
-
}
|
|
269
|
-
|
|
266
|
+
function verifyWebhookToken(input: WebhookVerificationInput): WebhookVerificationResult {
|
|
270
267
|
const expectedToken = input.secrets.ZAPSTER_WEBHOOK_TOKEN;
|
|
271
268
|
|
|
272
269
|
if (!expectedToken) {
|
package/src/runtime/config.ts
CHANGED
|
@@ -718,7 +718,7 @@ function validateWhatsappChannel(value: Record<string, unknown>, index: number):
|
|
|
718
718
|
if (value.provider === "zapster") {
|
|
719
719
|
expectRequiredSecrets(
|
|
720
720
|
value.secrets,
|
|
721
|
-
["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"],
|
|
721
|
+
["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"],
|
|
722
722
|
`channels[${index}]`,
|
|
723
723
|
);
|
|
724
724
|
return;
|
|
@@ -736,7 +736,7 @@ function validateWhatsappChannel(value: Record<string, unknown>, index: number):
|
|
|
736
736
|
if (value.provider === "uazapi") {
|
|
737
737
|
expectRequiredSecrets(
|
|
738
738
|
value.secrets,
|
|
739
|
-
["UAZAPI_BASE_URL", "UAZAPI_TOKEN"],
|
|
739
|
+
["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"],
|
|
740
740
|
`channels[${index}]`,
|
|
741
741
|
);
|
|
742
742
|
return;
|
|
@@ -55,6 +55,8 @@ export type AgentDevServer = {
|
|
|
55
55
|
|
|
56
56
|
const DEFAULT_PORT = 4123;
|
|
57
57
|
const DEFAULT_HOSTNAME = "localhost";
|
|
58
|
+
const MAX_JSON_BODY_BYTES = 1_048_576;
|
|
59
|
+
const MAX_WEBHOOK_BODY_BYTES = 1_048_576;
|
|
58
60
|
const CHANNEL_DEDUPE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
59
61
|
const CHANNEL_DEDUPE_MAX_KEYS = 10_000;
|
|
60
62
|
|
|
@@ -179,8 +181,8 @@ async function handleNodeRequest(
|
|
|
179
181
|
options: DevHttpServerOptions,
|
|
180
182
|
): Promise<void> {
|
|
181
183
|
try {
|
|
184
|
+
const request = nodeRequestToFetchRequest(incoming, options);
|
|
182
185
|
const capsule = await loadAgentCapsule(capsuleRoot);
|
|
183
|
-
const request = nodeRequestToFetchRequest(incoming);
|
|
184
186
|
const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
|
|
185
187
|
await writeFetchResponse(outgoing, response);
|
|
186
188
|
} catch (error) {
|
|
@@ -188,7 +190,7 @@ async function handleNodeRequest(
|
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
191
|
-
function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
|
|
193
|
+
function nodeRequestToFetchRequest(incoming: IncomingMessage, options: DevHttpServerOptions): Request {
|
|
192
194
|
const headers = new Headers();
|
|
193
195
|
|
|
194
196
|
for (const [key, value] of Object.entries(incoming.headers)) {
|
|
@@ -201,9 +203,24 @@ function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
|
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
const protocol = headers.get("x-forwarded-proto") ?? "http";
|
|
205
206
|
const host = headers.get("host") ?? `${DEFAULT_HOSTNAME}:${DEFAULT_PORT}`;
|
|
206
|
-
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;
|
|
207
224
|
const init: RequestInit & { duplex?: "half" } = {
|
|
208
225
|
method: incoming.method ?? "GET",
|
|
209
226
|
headers,
|
|
@@ -217,6 +234,28 @@ function nodeRequestToFetchRequest(incoming: IncomingMessage): Request {
|
|
|
217
234
|
return new Request(url, init);
|
|
218
235
|
}
|
|
219
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
|
+
|
|
220
259
|
async function writeFetchResponse(outgoing: ServerResponse, response: Response): Promise<void> {
|
|
221
260
|
outgoing.statusCode = response.status;
|
|
222
261
|
response.headers.forEach((value, key) => {
|
|
@@ -489,7 +528,7 @@ async function handlePortableChannel(
|
|
|
489
528
|
}
|
|
490
529
|
|
|
491
530
|
const adapter = adapterForChannel(route.type, route.provider);
|
|
492
|
-
const rawBody = request.method === "GET" ? "" : await request
|
|
531
|
+
const rawBody = request.method === "GET" ? "" : await readRequestTextWithLimit(request, MAX_WEBHOOK_BODY_BYTES);
|
|
493
532
|
const rawEvent: RawWebhookEvent = {
|
|
494
533
|
type: route.type,
|
|
495
534
|
provider: route.provider,
|
|
@@ -958,14 +997,19 @@ export class BoundedDedupeCache {
|
|
|
958
997
|
}
|
|
959
998
|
}
|
|
960
999
|
|
|
961
|
-
async function readRequestBytesWithLimit(
|
|
1000
|
+
async function readRequestBytesWithLimit(
|
|
1001
|
+
request: Request,
|
|
1002
|
+
maxBytes: number,
|
|
1003
|
+
errorCode = "file_too_large",
|
|
1004
|
+
label = "File",
|
|
1005
|
+
): Promise<Uint8Array> {
|
|
962
1006
|
const contentLength = request.headers.get("content-length");
|
|
963
1007
|
|
|
964
1008
|
if (contentLength) {
|
|
965
1009
|
const declaredBytes = Number(contentLength);
|
|
966
1010
|
|
|
967
1011
|
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
968
|
-
throw new AgentKitError(
|
|
1012
|
+
throw new AgentKitError(errorCode, `${label} exceeds the maximum size of ${maxBytes} bytes.`);
|
|
969
1013
|
}
|
|
970
1014
|
}
|
|
971
1015
|
|
|
@@ -988,7 +1032,7 @@ async function readRequestBytesWithLimit(request: Request, maxBytes: number): Pr
|
|
|
988
1032
|
|
|
989
1033
|
if (total > maxBytes) {
|
|
990
1034
|
await reader.cancel().catch(() => undefined);
|
|
991
|
-
throw new AgentKitError(
|
|
1035
|
+
throw new AgentKitError(errorCode, `${label} exceeds the maximum size of ${maxBytes} bytes.`);
|
|
992
1036
|
}
|
|
993
1037
|
|
|
994
1038
|
chunks.push(value);
|
|
@@ -1005,6 +1049,11 @@ async function readRequestBytesWithLimit(request: Request, maxBytes: number): Pr
|
|
|
1005
1049
|
return bytes;
|
|
1006
1050
|
}
|
|
1007
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
|
+
|
|
1008
1057
|
async function listConversations(capsule: LoadedAgentCapsule) {
|
|
1009
1058
|
const store = await openCapsuleStore(capsule);
|
|
1010
1059
|
|
|
@@ -1069,8 +1118,12 @@ async function readJsonBody(request: Request): Promise<unknown> {
|
|
|
1069
1118
|
}
|
|
1070
1119
|
|
|
1071
1120
|
try {
|
|
1072
|
-
return await request
|
|
1121
|
+
return JSON.parse(await readRequestTextWithLimit(request, MAX_JSON_BODY_BYTES));
|
|
1073
1122
|
} catch (error) {
|
|
1123
|
+
if (isAgentKitError(error)) {
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1074
1127
|
throw new AgentKitError("validation_error", "Request body must be valid JSON.", { cause: error });
|
|
1075
1128
|
}
|
|
1076
1129
|
}
|
|
@@ -1158,11 +1211,22 @@ function statusForError(error: unknown): number {
|
|
|
1158
1211
|
return 409;
|
|
1159
1212
|
}
|
|
1160
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
|
+
|
|
1161
1226
|
if (
|
|
1162
1227
|
error.code === "file_key_invalid" ||
|
|
1163
1228
|
error.code === "file_too_large" ||
|
|
1164
1229
|
error.code === "channel_payload_invalid" ||
|
|
1165
|
-
error.code === "channel_signature_invalid" ||
|
|
1166
1230
|
error.code === "channel_secret_missing"
|
|
1167
1231
|
) {
|
|
1168
1232
|
return 400;
|
|
@@ -91,7 +91,9 @@ export function createManagedComposioTools(
|
|
|
91
91
|
].join("\n\n"),
|
|
92
92
|
visibility: "user",
|
|
93
93
|
secrets: [MANAGED_COMPOSIO_API_KEY_SECRET],
|
|
94
|
-
permissions: integration.allowedTools.map(
|
|
94
|
+
permissions: integration.allowedTools.map(
|
|
95
|
+
(toolName) => `composio:${toolName.toLowerCase()}:${isComposioWriteAction(toolName) ? "write" : "read"}`,
|
|
96
|
+
),
|
|
95
97
|
timeoutMs: 60_000,
|
|
96
98
|
inputSchema: {
|
|
97
99
|
type: "object",
|
|
@@ -497,11 +497,11 @@ export function webhookOutputChannel(input) {
|
|
|
497
497
|
|
|
498
498
|
function defaultWhatsappChannelSecrets(provider) {
|
|
499
499
|
if (provider === "zapster") {
|
|
500
|
-
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
|
|
500
|
+
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID", "ZAPSTER_WEBHOOK_TOKEN"];
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
if (provider === "uazapi") {
|
|
504
|
-
return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"];
|
|
504
|
+
return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN", "UAZAPI_WEBHOOK_TOKEN"];
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
if (provider === "evolution") {
|
|
@@ -2124,6 +2124,7 @@ async function executeHostedTool(request) {
|
|
|
2124
2124
|
|
|
2125
2125
|
const input = normalizeOptionalNulls(request.input ?? {}, tool.inputSchema);
|
|
2126
2126
|
validateSchema(input, tool.inputSchema, \`tool "\${tool.name}" input\`);
|
|
2127
|
+
assertToolInvocationAuthorized(tool, request.invocation ?? "tool");
|
|
2127
2128
|
|
|
2128
2129
|
const secrets = resolveToolSecrets(tool, request.env);
|
|
2129
2130
|
const database = createHostedDatabaseRunner(request.env);
|
|
@@ -2167,6 +2168,23 @@ async function executeHostedTool(request) {
|
|
|
2167
2168
|
};
|
|
2168
2169
|
}
|
|
2169
2170
|
|
|
2171
|
+
function assertToolInvocationAuthorized(tool, invocation) {
|
|
2172
|
+
if (invocation === "tool") {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
const privilegedPermissions = (tool.permissions ?? []).filter((permission) => !permission.endsWith(":read"));
|
|
2177
|
+
|
|
2178
|
+
if (privilegedPermissions.length === 0) {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
throw agentKitError(
|
|
2183
|
+
"tool_authorization_required",
|
|
2184
|
+
\`Tool "\${tool.name}" requires an explicit operator invocation because it declares privileged permissions: \${privilegedPermissions.join(", ")}.\`,
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2170
2188
|
function failedHostedToolCall(tools, request, error) {
|
|
2171
2189
|
const tool = tools.find((candidate) => candidate.name === request.name);
|
|
2172
2190
|
|
package/src/runtime/tools.ts
CHANGED
|
@@ -81,6 +81,7 @@ async function executeToolCall(
|
|
|
81
81
|
|
|
82
82
|
input = normalizeOptionalNulls(input, tool.inputSchema);
|
|
83
83
|
validateSchema(input, tool.inputSchema, `tool "${tool.name}" input`);
|
|
84
|
+
assertToolInvocationAuthorized(tool, options.runtime);
|
|
84
85
|
|
|
85
86
|
const { secrets, secretValues } = resolveToolSecrets(tool, options.env ?? process.env);
|
|
86
87
|
const database = createLocalDatabaseRunner(options.store);
|
|
@@ -133,6 +134,23 @@ async function executeToolCall(
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
function assertToolInvocationAuthorized(tool: AgentTool, runtime: ToolRuntimeContext): void {
|
|
138
|
+
if (runtime.invocation === "tool") {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const privilegedPermissions = (tool.permissions ?? []).filter((permission) => !permission.endsWith(":read"));
|
|
143
|
+
|
|
144
|
+
if (privilegedPermissions.length === 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new AgentKitError(
|
|
149
|
+
"tool_authorization_required",
|
|
150
|
+
`Tool "${tool.name}" requires an explicit operator invocation because it declares privileged permissions: ${privilegedPermissions.join(", ")}. Run it with "agentkit tool ${tool.name} --input '<json>'" after reviewing the exact action.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
136
154
|
function renderToolOutput(
|
|
137
155
|
tool: AgentTool,
|
|
138
156
|
input: unknown,
|
|
@@ -7,7 +7,7 @@ description: Use when the owner gives a natural-language brief for a new or chan
|
|
|
7
7
|
|
|
8
8
|
Use this when the owner asks for an agent in plain language.
|
|
9
9
|
|
|
10
|
-
The owner should only need to run `agentkit new
|
|
10
|
+
The owner should only need to run `agentkit new [name]` or `agentkit new .`, open the capsule in Codex or another coding agent, and say what agent they want. Do not send them back to the CLI for a brief wizard. Treat their chat message as the brief and build the first useful local capsule.
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ The owner should only need to run `agentkit new <name>`, open the capsule in Cod
|
|
|
16
16
|
3. Infer the first useful local version from the owner's brief and the spec. Do not ask the owner to fill a form.
|
|
17
17
|
4. Edit `prompts/instructions.md` for behavior, boundaries, intake questions, escalation rules, and tool-use policy.
|
|
18
18
|
5. For scheduling, deadlines, reminders, or any relative-date behavior, set `timeZone` in `agentkit.config.ts` to the business/user timezone. AgentKit injects the current date, weekday, timestamp, and timezone dynamically at runtime; do not hardcode today's date in prompts.
|
|
19
|
-
6. Add tools
|
|
19
|
+
6. Add tools when the agent needs action, live data, authorization-sensitive data, durable writes, or a requested external judgment such as TypeSafe/Jev. Use `skills/agentkit-tools/SKILL.md` for the tool workflow and Jev guidance.
|
|
20
20
|
7. Add database tables to `schema.sql` or ordered `migrations/*.sql` when the agent owns records.
|
|
21
21
|
8. Add `sync.ts` and `seed.sql` with `npm run agentkit -- sync init` when the agent depends on external catalogs or recurring imports.
|
|
22
22
|
9. Turn requirements into checks as you build. Every privacy rule, external write, confirmation step, business-hour rule, timezone rule, required intake field, and customer-data boundary needs an eval, direct tool check, fixture, or deterministic fake path.
|
|
@@ -9,7 +9,7 @@ Use this first inside an AgentKit Agent Capsule.
|
|
|
9
9
|
|
|
10
10
|
## Owner-To-Codex Contract
|
|
11
11
|
|
|
12
|
-
The owner has already done the setup work by running `agentkit new
|
|
12
|
+
The owner has already done the setup work by running `agentkit new [name]` or `agentkit new .` and opening this folder in a coding agent. When they ask for an agent in natural language, that message is the brief.
|
|
13
13
|
|
|
14
14
|
- Do not ask the owner to run a wizard, fill a form, or prepare `AGENT_SPEC.md`.
|
|
15
15
|
- Route immediately to `skills/agentkit-build-agent/SKILL.md`.
|
|
@@ -39,7 +39,7 @@ If a real conversation reveals a bug or risky behavior, convert it into a regres
|
|
|
39
39
|
|
|
40
40
|
- Build or reshape the agent from the owner's brief: `skills/agentkit-build-agent/SKILL.md`
|
|
41
41
|
- Edit prompts: `skills/agentkit-prompts/SKILL.md`
|
|
42
|
-
- Add actions or
|
|
42
|
+
- Add actions, external data, or TypeSafe/Jev judgments: `skills/agentkit-tools/SKILL.md`
|
|
43
43
|
- Add AgentKit-managed integrations such as managed Composio: `skills/agentkit-integrations/SKILL.md`
|
|
44
44
|
- Add database tables or database-backed tools: `skills/agentkit-database/SKILL.md`
|
|
45
45
|
- Add docs, FAQs, prices, policies, or CSV facts: `skills/agentkit-knowledge/SKILL.md`
|
|
@@ -5,11 +5,6 @@ Required secrets:
|
|
|
5
5
|
```txt
|
|
6
6
|
UAZAPI_BASE_URL
|
|
7
7
|
UAZAPI_TOKEN
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
Optional hardening secret:
|
|
11
|
-
|
|
12
|
-
```txt
|
|
13
8
|
UAZAPI_WEBHOOK_TOKEN
|
|
14
9
|
```
|
|
15
10
|
|
|
@@ -18,15 +13,13 @@ Audio transcription also needs the configured transcription secret, usually `OPE
|
|
|
18
13
|
Commands:
|
|
19
14
|
|
|
20
15
|
```sh
|
|
21
|
-
agentkit
|
|
22
|
-
agentkit
|
|
23
|
-
agentkit
|
|
24
|
-
agentkit
|
|
25
|
-
agentkit channels test support-whatsapp --message "hello"
|
|
26
|
-
agentkit channels deliveries list support-whatsapp
|
|
16
|
+
node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))" | npm run agentkit -- env set UAZAPI_WEBHOOK_TOKEN --stdin
|
|
17
|
+
npm run agentkit -- env set UAZAPI_BASE_URL --stdin
|
|
18
|
+
npm run agentkit -- env set UAZAPI_TOKEN --stdin
|
|
19
|
+
npm run agentkit -- dev
|
|
27
20
|
```
|
|
28
21
|
|
|
29
|
-
AgentKit
|
|
22
|
+
`UAZAPI_WEBHOOK_TOKEN` is generated by the user for AgentKit; UAZAPI does not issue it. Expose the local port through an HTTPS tunnel and register the exact `/channels/support-whatsapp/whatsapp/uazapi/webhook?token=<UAZAPI_WEBHOOK_TOKEN>` URL with `addUrlEvents: false` and `addUrlTypesMessages: false`. Send a real message to confirm UAZAPI preserves the complete URL.
|
|
30
23
|
|
|
31
24
|
Inbound UAZAPI `messages` webhooks normalize text and audio messages. `fromMe` and `wasSentByApi` messages are skipped to avoid reply loops. Unsupported media should be logged as skipped/unsupported without creating an agent run.
|
|
32
25
|
|
|
@@ -6,11 +6,6 @@ Required secrets:
|
|
|
6
6
|
ZAPSTER_API_KEY
|
|
7
7
|
ZAPSTER_INSTANCE_ID
|
|
8
8
|
ZAPSTER_WEBHOOK_ID
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Optional hardening secret:
|
|
12
|
-
|
|
13
|
-
```txt
|
|
14
9
|
ZAPSTER_WEBHOOK_TOKEN
|
|
15
10
|
```
|
|
16
11
|
|
|
@@ -19,15 +14,14 @@ Audio transcription also needs the configured transcription secret, usually `OPE
|
|
|
19
14
|
Commands:
|
|
20
15
|
|
|
21
16
|
```sh
|
|
22
|
-
agentkit
|
|
23
|
-
agentkit
|
|
24
|
-
agentkit
|
|
25
|
-
agentkit
|
|
26
|
-
|
|
27
|
-
agentkit channels deliveries list support-whatsapp
|
|
17
|
+
node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))" | npm run agentkit -- env set ZAPSTER_WEBHOOK_TOKEN --stdin
|
|
18
|
+
npm run agentkit -- env set ZAPSTER_API_KEY --stdin
|
|
19
|
+
npm run agentkit -- env set ZAPSTER_INSTANCE_ID --stdin
|
|
20
|
+
npm run agentkit -- env set ZAPSTER_WEBHOOK_ID --stdin
|
|
21
|
+
npm run agentkit -- dev
|
|
28
22
|
```
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
`ZAPSTER_WEBHOOK_TOKEN` is generated by the user for AgentKit; Zapster does not issue it. Expose the local port through an HTTPS tunnel and register the exact `/channels/support-whatsapp/whatsapp/zapster/webhook?token=<ZAPSTER_WEBHOOK_TOKEN>` URL. Send a real message to confirm Zapster preserves the complete URL. Keep phone numbers redacted in logs by default.
|
|
31
25
|
|
|
32
26
|
AgentKit handles Zapster `message.received` envelopes with event id at `id`, message text at `data.content.text`, and contact identity at `data.sender.id`. Unsupported media should be logged as skipped/unsupported without creating an agent run.
|
|
33
27
|
|
|
@@ -42,7 +42,7 @@ Keep the action list explicit. Do not expose the whole Composio catalog by defau
|
|
|
42
42
|
|
|
43
43
|
For Google Calendar, do not configure create-only access. Include `GOOGLECALENDAR_EVENTS_LIST` so the agent can inspect availability before writing. For `GOOGLECALENDAR_CREATE_EVENT`, pass UTC `start_datetime` and explicit `event_duration_minutes` or `event_duration_hour`; AgentKit blocks Composio's implicit 30-minute duration default.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
An integration that allows managed Composio write actions is operator-only. AgentKit blocks its generated tool during model-driven chat, channel, and eval runs. Review the exact action, invoke it directly with `agentkit tool`, and pass `confirmed: true` for writes. `confirmExternalWrites: false` disables only that secondary input check, not the operator-only permission boundary.
|
|
46
46
|
|
|
47
47
|
## Testability
|
|
48
48
|
|
|
@@ -38,6 +38,8 @@ skills/
|
|
|
38
38
|
- Tools receive only secrets listed in that tool's `secrets` field.
|
|
39
39
|
- Prefer `ctx.secrets` over direct `process.env` reads in tools.
|
|
40
40
|
- Add `permissions` for external capabilities.
|
|
41
|
+
- Use a `:read` suffix only for read-only capabilities. AgentKit requires direct operator invocation for every other declared permission.
|
|
42
|
+
- Treat Capsule config, tools, evals, and sync modules as trusted executable TypeScript; local AgentKit commands do not sandbox them.
|
|
41
43
|
- Add timeouts to network tools.
|
|
42
44
|
- Remove client PII before writing evals.
|
|
43
45
|
- Keep `.agentkit/improve/` bundles out of commits and review generated regression evals before committing.
|
|
@@ -7,15 +7,19 @@ description: Use when adding, changing, registering, or testing AgentKit TypeScr
|
|
|
7
7
|
|
|
8
8
|
Use this when the agent needs code, an API, live data, a write, or an external action.
|
|
9
9
|
|
|
10
|
+
When the owner asks to use TypeSafe/Jev for a capsule capability, follow `docs/guides/use-jev.md` from the installed docs path (`npm run agentkit -- docs path`) and [the Jev tool example](examples/jev-service-fit.tool.md). Implement the requested judgment in a normal capsule tool; adapt its questions and criteria to the brief.
|
|
11
|
+
|
|
10
12
|
## Workflow
|
|
11
13
|
|
|
12
14
|
1. Create or edit `tools/<name>.ts`.
|
|
13
15
|
2. Export a `defineTool` tool with `name`, `description`, `inputSchema`, and usually `outputSchema`.
|
|
14
16
|
3. Add `secrets`, `permissions`, and `timeoutMs` when needed.
|
|
17
|
+
- End read-only permissions with `:read`.
|
|
18
|
+
- Non-read permissions are operator-only: chat, channels, and evals cannot execute them automatically.
|
|
15
19
|
4. Register the tool in `agentkit.config.ts`.
|
|
16
20
|
5. Keep secret names in `.env.schema`; values stay in ignored `.env` or hosted managed secrets.
|
|
17
21
|
6. Use `ctx.clock` for date-sensitive tool logic instead of calling `new Date()` directly.
|
|
18
|
-
7.
|
|
22
|
+
7. Verify destructive or external side effects through a reviewed direct `agentkit tool` invocation; evals should assert that automatic execution is blocked.
|
|
19
23
|
8. Add deterministic fixtures, fake branches, or direct tool inputs for important success and failure paths.
|
|
20
24
|
9. Add evals that assert the tool is called with safe inputs, or not called when confirmation/intake is missing.
|
|
21
25
|
|
|
@@ -7,23 +7,18 @@ import { defineTool } from "@andreprado/agentkit";
|
|
|
7
7
|
|
|
8
8
|
export const sendFollowupEmail = defineTool({
|
|
9
9
|
name: "send_followup_email",
|
|
10
|
-
description: "Sends a follow-up email after explicit
|
|
10
|
+
description: "Sends a follow-up email after explicit operator review.",
|
|
11
11
|
secrets: ["EMAIL_API_KEY"],
|
|
12
12
|
permissions: ["email:send"],
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: "object",
|
|
15
15
|
properties: {
|
|
16
16
|
email: { type: "string" },
|
|
17
|
-
confirmed: { type: "boolean" },
|
|
18
17
|
},
|
|
19
|
-
required: ["email"
|
|
18
|
+
required: ["email"],
|
|
20
19
|
additionalProperties: false,
|
|
21
20
|
},
|
|
22
|
-
async execute(input: { email: string
|
|
23
|
-
if (!input.confirmed) {
|
|
24
|
-
return { sent: false, reason: "confirmation_required" };
|
|
25
|
-
}
|
|
26
|
-
|
|
21
|
+
async execute(input: { email: string }, ctx) {
|
|
27
22
|
if (ctx.runtime.environment === "eval") {
|
|
28
23
|
return { sent: false, evalFixture: true, email: input.email };
|
|
29
24
|
}
|
|
@@ -35,3 +30,8 @@ export const sendFollowupEmail = defineTool({
|
|
|
35
30
|
});
|
|
36
31
|
```
|
|
37
32
|
|
|
33
|
+
Because `email:send` is not a `:read` permission, AgentKit rejects model-driven chat, channel, and eval calls before `execute` runs. After reviewing the exact recipient, the operator can invoke it directly:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm run agentkit -- tool send_followup_email --input '{"email":"client@example.com"}'
|
|
37
|
+
```
|