@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.
Files changed (34) hide show
  1. package/README.md +4 -1
  2. package/docs/guides/add-managed-composio.md +2 -2
  3. package/docs/guides/add-tool.md +4 -0
  4. package/docs/guides/channel-security.md +2 -0
  5. package/docs/guides/connect-whatsapp-uazapi.md +32 -19
  6. package/docs/guides/connect-whatsapp-zapster.md +35 -20
  7. package/docs/guides/create-agent.md +12 -0
  8. package/docs/guides/prepare-deploy.md +1 -1
  9. package/docs/guides/security-rules.md +3 -0
  10. package/docs/guides/use-jev.md +67 -0
  11. package/docs/llms-full.txt +17 -5
  12. package/docs/llms.txt +8 -2
  13. package/package.json +3 -4
  14. package/src/cli/commands/channels.ts +2 -2
  15. package/src/cli/help.ts +5 -3
  16. package/src/cli/index.ts +2 -5
  17. package/src/cli/new-command.ts +41 -0
  18. package/src/index.ts +2 -2
  19. package/src/runtime/channels/whatsapp-uazapi.ts +8 -12
  20. package/src/runtime/channels/whatsapp-zapster.ts +11 -14
  21. package/src/runtime/config.ts +2 -2
  22. package/src/runtime/dev-server.ts +74 -10
  23. package/src/runtime/integrations/composio.ts +3 -1
  24. package/src/runtime/targets/cloudflare/build.ts +20 -2
  25. package/src/runtime/tools.ts +18 -0
  26. package/src/templates/skills/agentkit-build-agent/SKILL.md +2 -2
  27. package/src/templates/skills/agentkit-capsule/SKILL.md +2 -2
  28. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +5 -12
  29. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +6 -12
  30. package/src/templates/skills/agentkit-integrations/SKILL.md +1 -1
  31. package/src/templates/skills/agentkit-security/SKILL.md +2 -0
  32. package/src/templates/skills/agentkit-tools/SKILL.md +5 -1
  33. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  34. 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 optionalTokenResult = verifyOptionalWebhookToken(input);
60
+ const tokenResult = verifyWebhookToken(input);
61
61
 
62
- if (!optionalTokenResult.ok) {
63
- return optionalTokenResult;
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((secret) => !input.secrets[secret]);
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 verifyOptionalWebhookToken(input: WebhookVerificationInput): WebhookVerificationResult {
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 optionalTokenResult = verifyOptionalWebhookToken(input);
37
+ const tokenResult = verifyWebhookToken(input);
38
38
 
39
- if (!optionalTokenResult.ok) {
40
- return optionalTokenResult;
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 = ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"].filter(
169
- (secret) => !input.secrets[secret],
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 verifyOptionalWebhookToken(input: WebhookVerificationInput): WebhookVerificationResult {
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) {
@@ -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 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;
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.text();
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(request: Request, maxBytes: number): Promise<Uint8Array> {
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("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
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("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
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.json();
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((toolName) => `composio:${toolName.toLowerCase()}`),
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
 
@@ -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 <name>`, 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.
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 only when the agent needs action, live data, authorization-sensitive data, or durable writes.
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 <name>` and opening this folder in a coding agent. When they ask for an agent in natural language, that message is the brief.
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 external data: `skills/agentkit-tools/SKILL.md`
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 deploy
22
- agentkit channels add whatsapp support-whatsapp --provider uazapi
23
- agentkit channels setup support-whatsapp --apply
24
- agentkit channels status support-whatsapp
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 applies UAZAPI setup by calling `/webhook` with the stable hosted URL and then confirming that UAZAPI lists the configured webhook. If the channel declares `UAZAPI_WEBHOOK_TOKEN`, AgentKit appends `?token=<UAZAPI_WEBHOOK_TOKEN>` to the registered webhook URL.
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 deploy
23
- agentkit channels add whatsapp support-whatsapp --provider zapster
24
- agentkit channels setup support-whatsapp
25
- agentkit channels status support-whatsapp
26
- agentkit channels test support-whatsapp --message "hello"
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
- Paste the stable AgentKit webhook URL into Zapster settings. If the channel declares `ZAPSTER_WEBHOOK_TOKEN`, append `?token=<ZAPSTER_WEBHOOK_TOKEN>` to the Zapster webhook URL. Keep phone numbers redacted in logs by default.
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
- Managed Composio write actions require tool input `confirmed: true` by default. Set it only after the owner/user confirms the exact external change. Use `confirmExternalWrites: false` only when the capsule implements an equivalent confirmation guard elsewhere.
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. Add eval guards for destructive or external side effects.
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 confirmation.",
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", "confirmed"],
18
+ required: ["email"],
20
19
  additionalProperties: false,
21
20
  },
22
- async execute(input: { email: string; confirmed: boolean }, ctx) {
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
+ ```