@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.
- 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 +34 -9
- 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 +33 -10
- 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"]);
|
|
@@ -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
|
|
112
|
-
configUrl.searchParams.set("t", String(Date.now()));
|
|
113
|
+
const source = await readFile(configPath, "utf8");
|
|
113
114
|
|
|
114
115
|
try {
|
|
115
|
-
|
|
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(
|
|
131
|
-
|
|
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}-${
|
|
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,
|
|
160
|
+
await writeFile(tempConfigPath, source);
|
|
153
161
|
const configUrl = pathToFileURL(tempConfigPath);
|
|
154
|
-
configUrl.searchParams.set("t",
|
|
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
|
-
|
|
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");
|