@andreprado/agentkit 0.1.0-alpha.14 → 0.1.0-alpha.15

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 (42) hide show
  1. package/README.md +2 -0
  2. package/docs/guides/run-evals.md +73 -25
  3. package/docs/llms-full.txt +24 -9
  4. package/docs/llms.txt +2 -0
  5. package/package.json +1 -1
  6. package/src/cli/cloud-client.ts +30 -10
  7. package/src/cli/deploy-readiness.ts +32 -11
  8. package/src/cli/index.ts +20 -6
  9. package/src/cloud/client.ts +4 -3
  10. package/src/cloud/contracts.ts +1 -1
  11. package/src/create-project.ts +1 -1
  12. package/src/index.ts +24 -1
  13. package/src/providers/pi.ts +14 -1
  14. package/src/providers/test.ts +36 -0
  15. package/src/runtime/chat.ts +59 -42
  16. package/src/runtime/config.ts +11 -0
  17. package/src/runtime/core/manifest.ts +3 -3
  18. package/src/runtime/deploy-readiness.ts +3 -3
  19. package/src/runtime/dev-server.ts +171 -13
  20. package/src/runtime/env.ts +8 -3
  21. package/src/runtime/evals.ts +404 -69
  22. package/src/runtime/inspect.ts +12 -0
  23. package/src/runtime/prompt-context.ts +141 -0
  24. package/src/runtime/runtime-contract.ts +17 -7
  25. package/src/runtime/targets/cloudflare/build.ts +23 -3
  26. package/src/runtime/targets/container/server.ts +1 -1
  27. package/src/runtime/targets/vps/deploy.ts +25 -8
  28. package/src/runtime/tool-runner.ts +7 -0
  29. package/src/runtime/tools.ts +8 -2
  30. package/src/templates/blank.ts +8 -3
  31. package/src/templates/dentista.ts +18 -10
  32. package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
  33. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  34. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
  35. package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
  36. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  37. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  38. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  39. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  40. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  41. package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
  42. package/src/templates/support.ts +8 -3
@@ -10,6 +10,7 @@ import { AgentKitError, isAgentKitError } from "./errors";
10
10
  import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
11
11
  import { appendKnowledgePromptPolicy } from "./knowledge/prompt-policy";
12
12
  import { createKnowledgeSearchTool } from "./knowledge/tool";
13
+ import { appendRuntimePromptContext, buildRuntimeClockContext } from "./prompt-context";
13
14
  import { createToolRuntime } from "./tools";
14
15
 
15
16
  export type RunAgentMessageOptions = {
@@ -17,6 +18,7 @@ export type RunAgentMessageOptions = {
17
18
  conversationId?: string;
18
19
  signal?: AbortSignal;
19
20
  runtime?: Partial<ToolRuntimeContext>;
21
+ now?: Date;
20
22
  };
21
23
 
22
24
  export type AgentRunResult = ProviderRunResult & PersistedChatRun;
@@ -41,51 +43,64 @@ export async function runAgentMessage(
41
43
  const adapter = getProviderAdapter(capsule.config.provider);
42
44
  const env = await loadCapsuleEnv(capsule.root);
43
45
  await syncConfiguredKnowledgeSources(capsule);
44
- const { store } = await openPreparedCapsuleStore(capsule);
45
46
  const conversationId = normalizeConversationId(options.conversationId);
46
- const { conversation, previousMessages } = resolveConversation(store, {
47
- agentName: capsule.config.name,
48
- conversationId,
49
- firstMessage: content,
50
- });
51
- const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
52
- const userMessage = store.appendMessage({
53
- conversationId: conversation.id,
54
- role: "user",
55
- content,
56
- });
57
- const run = store.createRun({
58
- conversationId: conversation.id,
59
- provider: capsule.config.provider.name,
60
- model: capsule.config.provider.model,
61
- });
62
- const runtimeTools = withKnowledgeTools(capsule);
63
- const toolRuntime = createToolRuntime({
64
- tools: runtimeTools,
65
- store,
66
- runId: run.id,
67
- conversationId: conversation.id,
68
- env,
69
- signal: options.signal,
70
- runtime: {
71
- environment: "local",
72
- invocation: "chat",
73
- target: "bun",
74
- database: "sqlite",
75
- ...options.runtime,
76
- },
77
- });
47
+ const { store } = await openPreparedCapsuleStore(capsule);
48
+ let runId: string | null = null;
78
49
 
79
50
  try {
51
+ const { conversation, previousMessages } = resolveConversation(store, {
52
+ agentName: capsule.config.name,
53
+ conversationId,
54
+ firstMessage: content,
55
+ });
56
+ const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
57
+ const userMessage = store.appendMessage({
58
+ conversationId: conversation.id,
59
+ role: "user",
60
+ content,
61
+ });
62
+ const run = store.createRun({
63
+ conversationId: conversation.id,
64
+ provider: capsule.config.provider.name,
65
+ model: capsule.config.provider.model,
66
+ });
67
+ runId = run.id;
68
+ const runtimeTools = withKnowledgeTools(capsule);
69
+ const clock = buildRuntimeClockContext({
70
+ timeZone: capsule.config.timeZone,
71
+ env,
72
+ now: options.now,
73
+ });
74
+ const toolRuntime = createToolRuntime({
75
+ tools: runtimeTools,
76
+ store,
77
+ runId: run.id,
78
+ conversationId: conversation.id,
79
+ env,
80
+ signal: options.signal,
81
+ clock,
82
+ runtime: {
83
+ environment: "local",
84
+ invocation: "chat",
85
+ target: "bun",
86
+ database: "sqlite",
87
+ ...options.runtime,
88
+ },
89
+ });
80
90
  const result = await adapter.run({
81
91
  agent: {
82
92
  name: capsule.config.name,
83
93
  runtime: capsule.config.runtime,
84
94
  provider: capsule.config.provider,
85
95
  },
86
- instructions: withToolVisibilityInstructions(
87
- appendKnowledgePromptPolicy(capsule.instructions, capsule.config),
88
- runtimeTools,
96
+ instructions: appendRuntimePromptContext(
97
+ withToolVisibilityInstructions(
98
+ appendKnowledgePromptPolicy(capsule.instructions, capsule.config),
99
+ runtimeTools,
100
+ ),
101
+ {
102
+ clock,
103
+ },
89
104
  ),
90
105
  messages,
91
106
  tools: toolRuntime.definitions,
@@ -114,12 +129,14 @@ export async function runAgentMessage(
114
129
  assistantMessageId: assistantMessage.id,
115
130
  };
116
131
  } catch (error) {
117
- store.completeRun({
118
- runId: run.id,
119
- status: "failed",
120
- errorCode: isAgentKitError(error) ? error.code : "runtime_error",
121
- errorMessage: error instanceof Error ? error.message : String(error),
122
- });
132
+ if (runId) {
133
+ store.completeRun({
134
+ runId,
135
+ status: "failed",
136
+ errorCode: isAgentKitError(error) ? error.code : "runtime_error",
137
+ errorMessage: error instanceof Error ? error.message : String(error),
138
+ });
139
+ }
123
140
 
124
141
  throw error;
125
142
  } finally {
@@ -18,6 +18,7 @@ import type {
18
18
  ChannelBufferMode,
19
19
  } from "../index";
20
20
  import { AgentKitError } from "./errors";
21
+ import { isValidTimeZone } from "./prompt-context";
21
22
 
22
23
  const CONFIG_FILE = "agentkit.config.ts";
23
24
  const AGENT_RUNTIMES = new Set<AgentRuntime>(["local", "edge"]);
@@ -207,8 +208,17 @@ function validateAgentConfig(value: unknown): asserts value is AgentConfig {
207
208
  throw invalidConfig('provider.model must be "fake" when provider.name is "test".');
208
209
  }
209
210
 
211
+ if (value.timeZone !== undefined) {
212
+ expectString(value.timeZone, "timeZone");
213
+
214
+ if (!isValidTimeZone(value.timeZone)) {
215
+ throw invalidConfig('timeZone must be a valid IANA time zone such as "UTC" or "America/New_York".');
216
+ }
217
+ }
218
+
210
219
  expectString(value.instructions, "instructions");
211
220
  expectStringArray(value.secrets, "secrets");
221
+ validateSecretNames(value.secrets, "secrets");
212
222
 
213
223
  if (value.tools !== undefined) {
214
224
  if (!Array.isArray(value.tools)) {
@@ -636,6 +646,7 @@ function validateTool(value: unknown, index: number): asserts value is AgentTool
636
646
 
637
647
  if (value.secrets !== undefined) {
638
648
  expectStringArray(value.secrets, `tools[${index}].secrets`);
649
+ validateSecretNames(value.secrets, `tools[${index}].secrets`);
639
650
  }
640
651
 
641
652
  if (value.permissions !== undefined) {
@@ -15,6 +15,7 @@ export type SharedAgentManifest = {
15
15
  name: string;
16
16
  runtime: AgentConfig["runtime"];
17
17
  provider: AgentConfig["provider"];
18
+ timeZone: string | null;
18
19
  prompt: string;
19
20
  secrets: string[];
20
21
  tools: Array<{
@@ -80,9 +81,7 @@ export function buildSharedAgentManifest(
80
81
  const files = buildFilesManifest(capsule);
81
82
  const knowledge = resolveKnowledgeConfig(capsule.config);
82
83
 
83
- if (capsule.config.access.mode === "private") {
84
- requiredSecrets.add("AGENTKIT_PRIVATE_TOKEN");
85
- }
84
+ requiredSecrets.add("AGENTKIT_PRIVATE_TOKEN");
86
85
 
87
86
  if (capsule.config.access.mode === "token") {
88
87
  requiredSecrets.add("AGENTKIT_ACCESS_TOKEN");
@@ -118,6 +117,7 @@ export function buildSharedAgentManifest(
118
117
  name: capsule.config.name,
119
118
  runtime: capsule.config.runtime,
120
119
  provider: capsule.config.provider,
120
+ timeZone: capsule.config.timeZone ?? null,
121
121
  prompt: toCapsulePath(capsule, capsule.instructionsPath),
122
122
  secrets: Array.from(requiredSecrets).sort(),
123
123
  tools: (capsule.config.tools ?? []).map((tool) => ({
@@ -85,8 +85,8 @@ export async function loadDeployReadinessContext(
85
85
  };
86
86
  }
87
87
 
88
- export function stableCloudProjectId(name: string): string {
89
- return `proj_${createHash("sha256").update(name).digest("hex").slice(0, 16)}`;
88
+ export function stableCloudProjectId(name: string, accountId?: string | null): string {
89
+ return `proj_${createHash("sha256").update(`${accountId ? `account:${accountId}` : "anonymous"}:${name}`).digest("hex").slice(0, 16)}`;
90
90
  }
91
91
 
92
92
  function isAgentKitManagedDeploySecret(
@@ -97,7 +97,7 @@ function isAgentKitManagedDeploySecret(
97
97
  return true;
98
98
  }
99
99
 
100
- if (context.accessMode === "private" && name === "AGENTKIT_PRIVATE_TOKEN") {
100
+ if (name === "AGENTKIT_PRIVATE_TOKEN") {
101
101
  return true;
102
102
  }
103
103
 
@@ -25,6 +25,7 @@ export type AgentDevServerOptions = {
25
25
  port?: number;
26
26
  hostname?: string;
27
27
  basePort?: number;
28
+ access?: AgentDevServerAccessPolicy;
28
29
  };
29
30
 
30
31
  export type AgentDevServer = {
@@ -49,6 +50,12 @@ export type AgentDevServer = {
49
50
  const DEFAULT_PORT = 4123;
50
51
  const DEFAULT_HOSTNAME = "localhost";
51
52
 
53
+ export type AgentDevServerAccessPolicy = "local" | "enforce";
54
+
55
+ type DevHttpServerOptions = {
56
+ access: AgentDevServerAccessPolicy;
57
+ };
58
+
52
59
  type DevChannelRuntimeState = {
53
60
  dedupeKeys: Set<string>;
54
61
  buffers: Map<string, DevChannelBuffer>;
@@ -74,7 +81,9 @@ export async function startAgentDevServer(
74
81
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
75
82
  const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
76
83
  const autoPort = options.port === undefined;
77
- const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
84
+ const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort, {
85
+ access: options.access ?? "local",
86
+ });
78
87
 
79
88
  if (start.actualPort === undefined) {
80
89
  throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
@@ -106,9 +115,10 @@ async function listenForDevServer(
106
115
  hostname: string,
107
116
  requestedPort: number,
108
117
  autoPort: boolean,
118
+ serverOptions: DevHttpServerOptions,
109
119
  ): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
110
120
  if (requestedPort === 0) {
111
- const server = createDevHttpServer(capsule);
121
+ const server = createDevHttpServer(capsule, serverOptions);
112
122
  const actualPort = await listen(server, hostname, await findAvailablePort());
113
123
  return { server, actualPort, portConflict: null };
114
124
  }
@@ -117,7 +127,7 @@ async function listenForDevServer(
117
127
  const lastPort = autoPort ? requestedPort + 50 : requestedPort;
118
128
 
119
129
  for (let port = requestedPort; port <= lastPort; port += 1) {
120
- const server = createDevHttpServer(capsule);
130
+ const server = createDevHttpServer(capsule, serverOptions);
121
131
 
122
132
  try {
123
133
  const actualPort = await listen(server, hostname, port);
@@ -142,14 +152,14 @@ async function listenForDevServer(
142
152
  );
143
153
  }
144
154
 
145
- function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
155
+ function createDevHttpServer(capsule: LoadedAgentCapsule, options: DevHttpServerOptions): Server {
146
156
  const channelRuntime: DevChannelRuntimeState = {
147
157
  dedupeKeys: new Set<string>(),
148
158
  buffers: new Map<string, DevChannelBuffer>(),
149
159
  };
150
160
  const capsuleRoot = capsule.root;
151
161
  return createHttpServer((incoming, outgoing) => {
152
- void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime);
162
+ void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime, options);
153
163
  });
154
164
  }
155
165
 
@@ -158,11 +168,12 @@ async function handleNodeRequest(
158
168
  incoming: IncomingMessage,
159
169
  outgoing: ServerResponse,
160
170
  channelRuntime: DevChannelRuntimeState,
171
+ options: DevHttpServerOptions,
161
172
  ): Promise<void> {
162
173
  try {
163
174
  const capsule = await loadAgentCapsule(capsuleRoot);
164
175
  const request = nodeRequestToFetchRequest(incoming);
165
- const response = await handleDevServerRequest(capsule, request, channelRuntime);
176
+ const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
166
177
  await writeFetchResponse(outgoing, response);
167
178
  } catch (error) {
168
179
  await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
@@ -222,27 +233,43 @@ async function handleDevServerRequest(
222
233
  capsule: LoadedAgentCapsule,
223
234
  request: Request,
224
235
  channelRuntime: DevChannelRuntimeState,
236
+ options: DevHttpServerOptions,
225
237
  ): Promise<Response> {
226
238
  try {
227
239
  const url = new URL(request.url);
228
240
  const route = normalizePath(url.pathname);
229
241
 
242
+ if (request.method === "GET" && route === "/health") {
243
+ return jsonResponse({ status: "ok", agent: capsule.config.name });
244
+ }
245
+
230
246
  if (request.method === "GET" && route === "/") {
247
+ const auth = await authorizeDevRoute(capsule, request, options, "chat");
248
+ if (auth) {
249
+ return auth;
250
+ }
251
+
231
252
  const env = await loadCapsuleEnv(capsule.root);
232
253
  return htmlResponse(renderChatApp(capsule, url.origin, env));
233
254
  }
234
255
 
235
256
  if (request.method === "GET" && route === "/_agentkit") {
257
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
258
+ if (auth) {
259
+ return auth;
260
+ }
261
+
236
262
  const env = await loadCapsuleEnv(capsule.root);
237
263
  return jsonResponse(buildInspectState(capsule, env));
238
264
  }
239
265
 
240
- if (request.method === "GET" && route === "/health") {
241
- return jsonResponse({ status: "ok", agent: capsule.config.name });
242
- }
243
-
244
266
  if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
245
267
  const body = await readJsonBody(request);
268
+ const auth = await authorizeDevRoute(capsule, request, options, "chat", body);
269
+ if (auth) {
270
+ return auth;
271
+ }
272
+
246
273
  const { message, conversationId } = readChatRequest(body);
247
274
  const result = await runAgentMessage(capsule, {
248
275
  message,
@@ -262,6 +289,11 @@ async function handleDevServerRequest(
262
289
  const toolMatch = route.match(/^\/tools\/([^/]+)$/);
263
290
 
264
291
  if (request.method === "POST" && toolMatch) {
292
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
293
+ if (auth) {
294
+ return auth;
295
+ }
296
+
265
297
  const body = await readJsonBody(request);
266
298
  const result = await runToolFromCwd(capsule.root, {
267
299
  name: decodeURIComponent(toolMatch[1]),
@@ -275,6 +307,11 @@ async function handleDevServerRequest(
275
307
  const fileMatch = route.match(/^\/files\/(.+)$/);
276
308
 
277
309
  if (fileMatch) {
310
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
311
+ if (auth) {
312
+ return auth;
313
+ }
314
+
278
315
  return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
279
316
  }
280
317
 
@@ -289,18 +326,33 @@ async function handleDevServerRequest(
289
326
  }
290
327
 
291
328
  if (request.method === "GET" && route === "/v1/conversations") {
329
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
330
+ if (auth) {
331
+ return auth;
332
+ }
333
+
292
334
  return jsonResponse({ conversations: await listConversations(capsule) });
293
335
  }
294
336
 
295
337
  const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
296
338
 
297
339
  if (request.method === "GET" && conversationMatch) {
340
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
341
+ if (auth) {
342
+ return auth;
343
+ }
344
+
298
345
  return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
299
346
  }
300
347
 
301
348
  const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
302
349
 
303
350
  if (request.method === "GET" && conversationTraceMatch) {
351
+ const auth = await authorizeDevRoute(capsule, request, options, "admin");
352
+ if (auth) {
353
+ return auth;
354
+ }
355
+
304
356
  return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
305
357
  }
306
358
 
@@ -310,6 +362,109 @@ async function handleDevServerRequest(
310
362
  }
311
363
  }
312
364
 
365
+ async function authorizeDevRoute(
366
+ capsule: LoadedAgentCapsule,
367
+ request: Request,
368
+ options: DevHttpServerOptions,
369
+ kind: "chat" | "admin",
370
+ body?: unknown,
371
+ ): Promise<Response | null> {
372
+ if (options.access === "local") {
373
+ return null;
374
+ }
375
+
376
+ const env = await loadCapsuleEnv(capsule.root);
377
+
378
+ if (kind === "admin") {
379
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
380
+ missingCode: "deploy_not_ready",
381
+ missingMessage: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set.",
382
+ unauthorizedMessage: "Missing or invalid AgentKit admin token.",
383
+ });
384
+ }
385
+
386
+ if (capsule.config.access.mode === "public") {
387
+ return null;
388
+ }
389
+
390
+ if (capsule.config.access.mode === "private") {
391
+ return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
392
+ missingCode: "deploy_not_ready",
393
+ missingMessage: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set.",
394
+ unauthorizedMessage: "Missing or invalid deploy access token.",
395
+ });
396
+ }
397
+
398
+ return authorizeBearerSecret(request, body, env.AGENTKIT_ACCESS_TOKEN, {
399
+ missingCode: "deploy_not_ready",
400
+ missingMessage: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set.",
401
+ unauthorizedMessage: "Missing or invalid deploy access token.",
402
+ });
403
+ }
404
+
405
+ function authorizeBearerSecret(
406
+ request: Request,
407
+ body: unknown,
408
+ secret: string | undefined,
409
+ messages: { missingCode: string; missingMessage: string; unauthorizedMessage: string },
410
+ ): Response | null {
411
+ if (!secret) {
412
+ return jsonResponse(
413
+ {
414
+ error: {
415
+ code: messages.missingCode,
416
+ message: messages.missingMessage,
417
+ },
418
+ },
419
+ { status: 503 },
420
+ );
421
+ }
422
+
423
+ if (readRequestAccessToken(request, body) !== secret) {
424
+ return jsonResponse(
425
+ {
426
+ error: {
427
+ code: "unauthorized",
428
+ message: messages.unauthorizedMessage,
429
+ },
430
+ },
431
+ { status: 401 },
432
+ );
433
+ }
434
+
435
+ return null;
436
+ }
437
+
438
+ function readRequestAccessToken(request: Request, body: unknown): string {
439
+ const authorization = request.headers.get("authorization");
440
+
441
+ if (authorization?.startsWith("Bearer ")) {
442
+ return authorization.slice("Bearer ".length).trim();
443
+ }
444
+
445
+ const header =
446
+ request.headers.get("x-agentkit-access-token") ??
447
+ request.headers.get("x-agentkit-deploy-access-token") ??
448
+ "";
449
+ const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
450
+ if (token.trim()) {
451
+ return token.trim();
452
+ }
453
+
454
+ if (body && typeof body === "object") {
455
+ const record = body as { access_token?: unknown; accessToken?: unknown };
456
+ const bodyToken =
457
+ typeof record.access_token === "string"
458
+ ? record.access_token
459
+ : typeof record.accessToken === "string"
460
+ ? record.accessToken
461
+ : "";
462
+ return bodyToken.trim();
463
+ }
464
+
465
+ return "";
466
+ }
467
+
313
468
  async function handlePortableChannel(
314
469
  capsule: LoadedAgentCapsule,
315
470
  request: Request,
@@ -509,6 +664,10 @@ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
509
664
  }
510
665
 
511
666
  async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
667
+ if (capsule.config.storage.files?.driver !== "r2") {
668
+ throw new AgentKitError("files_not_configured", "File storage is not configured for this Agent Capsule.");
669
+ }
670
+
512
671
  const key = normalizeFileKey(rawKey);
513
672
  const root = resolve(capsule.root, ".agentkit/files");
514
673
  const path = resolve(root, key);
@@ -519,9 +678,7 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
519
678
 
520
679
  if (request.method === "PUT") {
521
680
  const bytes = new Uint8Array(await request.arrayBuffer());
522
- const maxUploadBytes = capsule.config.storage.files?.driver === "r2"
523
- ? capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024
524
- : 10 * 1024 * 1024;
681
+ const maxUploadBytes = capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024;
525
682
 
526
683
  if (bytes.byteLength > maxUploadBytes) {
527
684
  throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
@@ -715,6 +872,7 @@ function statusForError(error: unknown): number {
715
872
  error.code === "conversation_not_found" ||
716
873
  error.code === "not_found" ||
717
874
  error.code === "tool_not_found" ||
875
+ error.code === "files_not_configured" ||
718
876
  error.code === "file_not_found" ||
719
877
  error.code === "channel_not_found"
720
878
  ) {
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { chmod, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { AgentKitError } from "./errors";
@@ -83,7 +83,7 @@ export async function setCapsuleEnv(root: string, name: string, value: string):
83
83
  }
84
84
  }
85
85
 
86
- await writeFile(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
86
+ await writeSecureDotEnv(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
87
87
  }
88
88
 
89
89
  export async function unsetCapsuleEnv(root: string, name: string): Promise<boolean> {
@@ -103,10 +103,15 @@ export async function unsetCapsuleEnv(root: string, name: string): Promise<boole
103
103
  return false;
104
104
  }
105
105
 
106
- await writeFile(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
106
+ await writeSecureDotEnv(path, `${nextLines.join("\n").replace(/\n*$/, "")}\n`);
107
107
  return true;
108
108
  }
109
109
 
110
+ async function writeSecureDotEnv(path: string, source: string): Promise<void> {
111
+ await writeFile(path, source, { mode: 0o600 });
112
+ await chmod(path, 0o600);
113
+ }
114
+
110
115
  async function readDotEnvSource(path: string): Promise<string> {
111
116
  try {
112
117
  return await readFile(path, "utf8");