@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.
- package/README.md +2 -0
- package/docs/guides/run-evals.md +73 -25
- package/docs/llms-full.txt +24 -9
- package/docs/llms.txt +2 -0
- package/package.json +1 -1
- package/src/cli/cloud-client.ts +30 -10
- package/src/cli/deploy-readiness.ts +32 -11
- package/src/cli/index.ts +20 -6
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +1 -1
- package/src/index.ts +24 -1
- package/src/providers/pi.ts +14 -1
- package/src/providers/test.ts +36 -0
- package/src/runtime/chat.ts +59 -42
- package/src/runtime/config.ts +11 -0
- package/src/runtime/core/manifest.ts +3 -3
- package/src/runtime/deploy-readiness.ts +3 -3
- package/src/runtime/dev-server.ts +171 -13
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +404 -69
- package/src/runtime/inspect.ts +12 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +17 -7
- package/src/runtime/targets/cloudflare/build.ts +23 -3
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +25 -8
- package/src/runtime/tool-runner.ts +7 -0
- package/src/runtime/tools.ts +8 -2
- package/src/templates/blank.ts +8 -3
- package/src/templates/dentista.ts +18 -10
- package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
- package/src/templates/support.ts +8 -3
package/src/runtime/chat.ts
CHANGED
|
@@ -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 {
|
|
47
|
-
|
|
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:
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 {
|
package/src/runtime/config.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
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
|
) {
|
package/src/runtime/env.ts
CHANGED
|
@@ -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
|
|
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
|
|
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");
|