@andreprado/agentkit 0.1.0-alpha.13 → 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 +34 -9
  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 +33 -10
  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"]);
@@ -32,6 +33,7 @@ const KNOWLEDGE_EMBEDDING_PROVIDERS = new Set<AgentKnowledgeEmbeddingProvider>([
32
33
  const CHANNEL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
33
34
  const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
34
35
  const moduleDir = dirname(fileURLToPath(import.meta.url));
36
+ let configImportNonce = 0;
35
37
 
36
38
  export type LoadedAgentCapsule = {
37
39
  root: string;
@@ -108,15 +110,13 @@ function resolveLocalStoragePath(root: string, config: AgentConfig): string | un
108
110
  }
109
111
 
110
112
  async function loadConfigModule(configPath: string): Promise<unknown> {
111
- const configUrl = pathToFileURL(configPath);
112
- configUrl.searchParams.set("t", String(Date.now()));
113
+ const source = await readFile(configPath, "utf8");
113
114
 
114
115
  try {
115
- const module = (await import(configUrl.href)) as { default?: unknown };
116
- return module.default;
116
+ return await importConfigSource(configPath, source);
117
117
  } catch (error) {
118
118
  if (isMissingAgentKitSelfImport(error)) {
119
- return loadConfigModuleWithLocalSelfImport(configPath, error);
119
+ return loadConfigModuleWithLocalSelfImport(configPath, source, error);
120
120
  }
121
121
 
122
122
  throw new AgentKitError(
@@ -127,8 +127,11 @@ async function loadConfigModule(configPath: string): Promise<unknown> {
127
127
  }
128
128
  }
129
129
 
130
- async function loadConfigModuleWithLocalSelfImport(configPath: string, originalError: unknown): Promise<unknown> {
131
- const source = await readFile(configPath, "utf8");
130
+ async function loadConfigModuleWithLocalSelfImport(
131
+ configPath: string,
132
+ source: string,
133
+ originalError: unknown,
134
+ ): Promise<unknown> {
132
135
  const localAgentKitUrl = pathToFileURL(resolve(moduleDir, "../index.ts")).href;
133
136
  const patchedSource = source.replace(
134
137
  /from\s+["']@andreprado\/agentkit["']/g,
@@ -143,15 +146,20 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
143
146
  );
144
147
  }
145
148
 
149
+ return importConfigSource(configPath, patchedSource);
150
+ }
151
+
152
+ async function importConfigSource(configPath: string, source: string): Promise<unknown> {
153
+ const nonce = nextConfigImportNonce();
146
154
  const tempConfigPath = join(
147
155
  dirname(configPath),
148
- `.agentkit.config.loader-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.ts`,
156
+ `.agentkit.config.loader-${process.pid}-${nonce}-${Math.random().toString(16).slice(2)}.ts`,
149
157
  );
150
158
 
151
159
  try {
152
- await writeFile(tempConfigPath, patchedSource);
160
+ await writeFile(tempConfigPath, source);
153
161
  const configUrl = pathToFileURL(tempConfigPath);
154
- configUrl.searchParams.set("t", String(Date.now()));
162
+ configUrl.searchParams.set("t", nonce);
155
163
  const module = (await import(configUrl.href)) as { default?: unknown };
156
164
  return module.default;
157
165
  } catch (error) {
@@ -165,6 +173,11 @@ async function loadConfigModuleWithLocalSelfImport(configPath: string, originalE
165
173
  }
166
174
  }
167
175
 
176
+ function nextConfigImportNonce(): string {
177
+ configImportNonce += 1;
178
+ return `${Date.now()}-${configImportNonce}`;
179
+ }
180
+
168
181
  function isMissingAgentKitSelfImport(error: unknown): boolean {
169
182
  const message = error instanceof Error ? error.message : String(error);
170
183
  return message.includes("@andreprado/agentkit");
@@ -195,8 +208,17 @@ function validateAgentConfig(value: unknown): asserts value is AgentConfig {
195
208
  throw invalidConfig('provider.model must be "fake" when provider.name is "test".');
196
209
  }
197
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
+
198
219
  expectString(value.instructions, "instructions");
199
220
  expectStringArray(value.secrets, "secrets");
221
+ validateSecretNames(value.secrets, "secrets");
200
222
 
201
223
  if (value.tools !== undefined) {
202
224
  if (!Array.isArray(value.tools)) {
@@ -624,6 +646,7 @@ function validateTool(value: unknown, index: number): asserts value is AgentTool
624
646
 
625
647
  if (value.secrets !== undefined) {
626
648
  expectStringArray(value.secrets, `tools[${index}].secrets`);
649
+ validateSecretNames(value.secrets, `tools[${index}].secrets`);
627
650
  }
628
651
 
629
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");