@librechat/agents 3.1.90 → 3.1.91
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/dist/cjs/agents/AgentContext.cjs +9 -5
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +46 -14
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +234 -0
- package/dist/cjs/langfuse.cjs.map +1 -0
- package/dist/cjs/main.cjs +25 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +44 -27
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +10 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +9 -5
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +46 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +226 -0
- package/dist/esm/langfuse.mjs.map +1 -0
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/run.mjs +44 -27
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +10 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/graphs/Graph.d.ts +6 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/langfuse.d.ts +48 -0
- package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
- package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
- package/dist/types/tools/cloudflare/index.d.ts +4 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
- package/dist/types/types/graph.d.ts +8 -0
- package/dist/types/types/tools.d.ts +118 -2
- package/package.json +4 -4
- package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
- package/src/agents/AgentContext.ts +13 -3
- package/src/graphs/Graph.ts +53 -16
- package/src/index.ts +1 -0
- package/src/langfuse.ts +358 -0
- package/src/run.ts +60 -38
- package/src/specs/langfuse-config.test.ts +57 -0
- package/src/specs/langfuse-metadata.test.ts +19 -1
- package/src/stream.ts +13 -3
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
- package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
- package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
- package/src/tools/cloudflare/index.ts +4 -0
- package/src/tools/local/LocalExecutionEngine.ts +20 -4
- package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
- package/src/types/graph.ts +9 -0
- package/src/types/tools.ts +141 -2
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { PassThrough } from 'stream';
|
|
3
|
+
import { posix as path } from 'path';
|
|
4
|
+
import type { ChildProcessWithoutNullStreams } from 'child_process';
|
|
5
|
+
import type { WriteFileOptions, MakeDirectoryOptions, Stats } from 'fs';
|
|
6
|
+
import type { FileHandle } from 'fs/promises';
|
|
7
|
+
import type * as t from '@/types';
|
|
8
|
+
import {
|
|
9
|
+
LOCAL_SPAWN_TIMEOUT_MS,
|
|
10
|
+
validateBashCommand,
|
|
11
|
+
} from '@/tools/local/LocalExecutionEngine';
|
|
12
|
+
import type { WorkspaceFS, ReaddirEntry } from '@/tools/local/workspaceFS';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_WORKSPACE_ROOT = '/workspace';
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
16
|
+
const DEFAULT_MAX_OUTPUT_CHARS = 200000;
|
|
17
|
+
const PROTECTED_TARGET_ARG_RE = /^(?:\/|~|\$\{?HOME\}?|\.)(?:\/?\.?\*|\/)?$/;
|
|
18
|
+
const DESTRUCTIVE_OP_IN_COMMAND_RE =
|
|
19
|
+
/\b(?:rm\s+-[^\s]*[rf]|chmod\s+-R|chown\s+-R)\b/;
|
|
20
|
+
|
|
21
|
+
type SpawnResult = {
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
exitCode: number | null;
|
|
25
|
+
timedOut: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type RuntimeCommand = {
|
|
29
|
+
fileName: string;
|
|
30
|
+
source?: string;
|
|
31
|
+
command: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type SandboxRuntimeContext = {
|
|
35
|
+
sandbox: t.CloudflareSandboxRuntime;
|
|
36
|
+
workspaceRoot: string;
|
|
37
|
+
env?: Record<string, string | undefined>;
|
|
38
|
+
timeoutMs: number;
|
|
39
|
+
maxOutputChars: number;
|
|
40
|
+
shell: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const sandboxFactoryCache = new WeakMap<
|
|
44
|
+
t.CloudflareSandboxExecutionConfig,
|
|
45
|
+
Promise<t.CloudflareSandboxRuntime>
|
|
46
|
+
>();
|
|
47
|
+
|
|
48
|
+
function normalizeWorkspaceRoot(workspaceRoot: string): string {
|
|
49
|
+
const normalized = path.normalize(workspaceRoot);
|
|
50
|
+
return normalized === '/' ? normalized : normalized.replace(/\/+$/, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getCloudflareWorkspaceRoot(
|
|
54
|
+
config?: t.CloudflareSandboxExecutionConfig
|
|
55
|
+
): string {
|
|
56
|
+
return normalizeWorkspaceRoot(
|
|
57
|
+
config?.workspaceRoot ?? DEFAULT_WORKSPACE_ROOT
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function resolveCloudflareSandbox(
|
|
62
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
63
|
+
): Promise<t.CloudflareSandboxRuntime> {
|
|
64
|
+
const sandbox = config.sandbox;
|
|
65
|
+
if (typeof sandbox !== 'function') {
|
|
66
|
+
return sandbox;
|
|
67
|
+
}
|
|
68
|
+
let cached = sandboxFactoryCache.get(config);
|
|
69
|
+
if (cached == null) {
|
|
70
|
+
cached = Promise.resolve()
|
|
71
|
+
.then(() => sandbox())
|
|
72
|
+
.catch((error: unknown) => {
|
|
73
|
+
sandboxFactoryCache.delete(config);
|
|
74
|
+
throw error;
|
|
75
|
+
});
|
|
76
|
+
sandboxFactoryCache.set(config, cached);
|
|
77
|
+
}
|
|
78
|
+
return cached;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getRuntimeContext(
|
|
82
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
83
|
+
): Promise<SandboxRuntimeContext> {
|
|
84
|
+
return {
|
|
85
|
+
sandbox: await resolveCloudflareSandbox(config),
|
|
86
|
+
workspaceRoot: getCloudflareWorkspaceRoot(config),
|
|
87
|
+
env: config.env,
|
|
88
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
89
|
+
maxOutputChars: config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS,
|
|
90
|
+
shell: config.shell ?? 'bash',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toSandboxPath(filePath: string, workspaceRoot: string): string {
|
|
95
|
+
const raw = filePath === '' ? '.' : filePath;
|
|
96
|
+
const root = normalizeWorkspaceRoot(workspaceRoot);
|
|
97
|
+
const resolved = raw.startsWith('/')
|
|
98
|
+
? path.normalize(raw)
|
|
99
|
+
: path.resolve(root, raw);
|
|
100
|
+
if (root === '/') {
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
if (resolved === root || resolved.startsWith(`${root}/`)) {
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Path is outside the Cloudflare sandbox workspace: ${filePath}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function quote(value: string): string {
|
|
112
|
+
if (value === '') {
|
|
113
|
+
return '\'\'';
|
|
114
|
+
}
|
|
115
|
+
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
return `'${value.replace(/'/g, '\'\\\'\'')}'`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function withInSandboxTimeout(command: string, timeoutMs: number): string {
|
|
122
|
+
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
123
|
+
return `timeout -k 2s ${timeoutSeconds}s ${command}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function outerTimeoutMs(timeoutMs: number): number {
|
|
127
|
+
return timeoutMs + 5000;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isInSandboxTimeoutExit(exitCode: number | null): boolean {
|
|
131
|
+
return exitCode === 124 || exitCode === 137;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function truncateOutput(value: string, maxChars: number): string {
|
|
135
|
+
if (maxChars <= 0 || value.length <= maxChars) {
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
const head = Math.max(Math.floor(maxChars / 2), 0);
|
|
139
|
+
const tail = Math.max(maxChars - head, 0);
|
|
140
|
+
return `${value.slice(0, head)}\n...[truncated ${value.length - maxChars} chars]...\n${value.slice(value.length - tail)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readStream(stream: ReadableStream<Uint8Array>): Promise<Buffer> {
|
|
144
|
+
const reader = stream.getReader();
|
|
145
|
+
const chunks: Uint8Array[] = [];
|
|
146
|
+
try {
|
|
147
|
+
for (;;) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done) break;
|
|
150
|
+
chunks.push(value);
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
reader.releaseLock();
|
|
154
|
+
}
|
|
155
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function normalizeReadFileContent(
|
|
159
|
+
result: t.CloudflareSandboxReadFileResult
|
|
160
|
+
): Promise<Buffer> {
|
|
161
|
+
if (typeof result === 'string') {
|
|
162
|
+
return Buffer.from(result, 'utf8');
|
|
163
|
+
}
|
|
164
|
+
if (Buffer.isBuffer(result)) {
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
if (result instanceof Uint8Array) {
|
|
168
|
+
return Buffer.from(result);
|
|
169
|
+
}
|
|
170
|
+
const content = result.content;
|
|
171
|
+
if (typeof content === 'string') {
|
|
172
|
+
if (result.encoding === 'base64') {
|
|
173
|
+
return Buffer.from(content, 'base64');
|
|
174
|
+
}
|
|
175
|
+
return Buffer.from(content, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
if (Buffer.isBuffer(content)) {
|
|
178
|
+
return content;
|
|
179
|
+
}
|
|
180
|
+
if (content instanceof Uint8Array) {
|
|
181
|
+
return Buffer.from(content);
|
|
182
|
+
}
|
|
183
|
+
return readStream(content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
187
|
+
return new ReadableStream<Uint8Array>({
|
|
188
|
+
start(controller): void {
|
|
189
|
+
controller.enqueue(bytes);
|
|
190
|
+
controller.close();
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeWriteFileContent(content: string | Buffer | Uint8Array): {
|
|
196
|
+
content: string | ReadableStream<Uint8Array>;
|
|
197
|
+
options?: { encoding?: string };
|
|
198
|
+
} {
|
|
199
|
+
if (typeof content === 'string') {
|
|
200
|
+
return { content, options: { encoding: 'utf8' } };
|
|
201
|
+
}
|
|
202
|
+
return { content: bytesToStream(content) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createStats(info: {
|
|
206
|
+
size?: number;
|
|
207
|
+
type?: t.CloudflareSandboxFileInfo['type'];
|
|
208
|
+
}): Stats {
|
|
209
|
+
const type = info.type ?? 'file';
|
|
210
|
+
const now = new Date();
|
|
211
|
+
return {
|
|
212
|
+
size: info.size ?? 0,
|
|
213
|
+
isFile: () => type === 'file',
|
|
214
|
+
isDirectory: () => type === 'directory',
|
|
215
|
+
isSymbolicLink: () => type === 'symlink',
|
|
216
|
+
isBlockDevice: () => false,
|
|
217
|
+
isCharacterDevice: () => false,
|
|
218
|
+
isFIFO: () => false,
|
|
219
|
+
isSocket: () => false,
|
|
220
|
+
dev: 0,
|
|
221
|
+
ino: 0,
|
|
222
|
+
mode: 0,
|
|
223
|
+
nlink: 1,
|
|
224
|
+
uid: 0,
|
|
225
|
+
gid: 0,
|
|
226
|
+
rdev: 0,
|
|
227
|
+
blksize: 0,
|
|
228
|
+
blocks: 0,
|
|
229
|
+
atimeMs: now.getTime(),
|
|
230
|
+
mtimeMs: now.getTime(),
|
|
231
|
+
ctimeMs: now.getTime(),
|
|
232
|
+
birthtimeMs: now.getTime(),
|
|
233
|
+
atime: now,
|
|
234
|
+
mtime: now,
|
|
235
|
+
ctime: now,
|
|
236
|
+
birthtime: now,
|
|
237
|
+
} as Stats;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeFileList(
|
|
241
|
+
result: t.CloudflareSandboxListFilesResult
|
|
242
|
+
): t.CloudflareSandboxFileInfo[] {
|
|
243
|
+
return Array.isArray(result) ? result : result.files;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function entryNameFor(
|
|
247
|
+
info: t.CloudflareSandboxFileInfo,
|
|
248
|
+
parentPath: string
|
|
249
|
+
): string {
|
|
250
|
+
if (info.name !== '') {
|
|
251
|
+
return info.name.includes('/') ? path.basename(info.name) : info.name;
|
|
252
|
+
}
|
|
253
|
+
if (info.absolutePath != null && info.absolutePath !== '') {
|
|
254
|
+
return path.basename(info.absolutePath);
|
|
255
|
+
}
|
|
256
|
+
if (info.relativePath != null && info.relativePath !== '') {
|
|
257
|
+
return path.basename(info.relativePath);
|
|
258
|
+
}
|
|
259
|
+
return path.basename(parentPath);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function entryAbsolutePath(
|
|
263
|
+
info: t.CloudflareSandboxFileInfo,
|
|
264
|
+
parentPath: string
|
|
265
|
+
): string {
|
|
266
|
+
if (info.absolutePath != null && info.absolutePath !== '') {
|
|
267
|
+
return path.normalize(info.absolutePath);
|
|
268
|
+
}
|
|
269
|
+
if (info.relativePath != null && info.relativePath !== '') {
|
|
270
|
+
return path.resolve(parentPath, info.relativePath);
|
|
271
|
+
}
|
|
272
|
+
return path.resolve(parentPath, info.name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function createDirent(info: t.CloudflareSandboxFileInfo): ReaddirEntry {
|
|
276
|
+
return {
|
|
277
|
+
name: entryNameFor(info, ''),
|
|
278
|
+
isFile: () => (info.type ?? 'file') === 'file',
|
|
279
|
+
isDirectory: () => info.type === 'directory',
|
|
280
|
+
isSymbolicLink: () => info.type === 'symlink',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function findChildInfo(
|
|
285
|
+
sandbox: t.CloudflareSandboxRuntime,
|
|
286
|
+
filePath: string
|
|
287
|
+
): Promise<t.CloudflareSandboxFileInfo | undefined> {
|
|
288
|
+
const parent = path.dirname(filePath);
|
|
289
|
+
const basename = path.basename(filePath);
|
|
290
|
+
const entries = normalizeFileList(
|
|
291
|
+
await sandbox.listFiles(parent, { includeHidden: true })
|
|
292
|
+
);
|
|
293
|
+
return entries.find((entry) => {
|
|
294
|
+
const absolute = entryAbsolutePath(entry, parent);
|
|
295
|
+
return absolute === filePath || entryNameFor(entry, parent) === basename;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function createCloudflareWorkspaceFS(
|
|
300
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
301
|
+
): WorkspaceFS {
|
|
302
|
+
const workspaceRoot = getCloudflareWorkspaceRoot(config);
|
|
303
|
+
|
|
304
|
+
const fs: WorkspaceFS = {
|
|
305
|
+
readFile: (async (filePath: string, encoding?: 'utf8') => {
|
|
306
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
307
|
+
const resolved = toSandboxPath(filePath, workspaceRoot);
|
|
308
|
+
const buffer = await normalizeReadFileContent(
|
|
309
|
+
await sandbox.readFile(resolved, encoding ? { encoding } : undefined)
|
|
310
|
+
);
|
|
311
|
+
return encoding != null ? buffer.toString(encoding) : buffer;
|
|
312
|
+
}) as WorkspaceFS['readFile'],
|
|
313
|
+
writeFile: async (
|
|
314
|
+
filePath: string,
|
|
315
|
+
content: string | Buffer,
|
|
316
|
+
_options?: WriteFileOptions
|
|
317
|
+
) => {
|
|
318
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
319
|
+
const resolved = toSandboxPath(filePath, workspaceRoot);
|
|
320
|
+
const normalized = normalizeWriteFileContent(content);
|
|
321
|
+
await sandbox.writeFile(resolved, normalized.content, normalized.options);
|
|
322
|
+
},
|
|
323
|
+
stat: async (filePath: string) => {
|
|
324
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
325
|
+
const resolved = toSandboxPath(filePath, workspaceRoot);
|
|
326
|
+
if (resolved === workspaceRoot) {
|
|
327
|
+
const entries = normalizeFileList(
|
|
328
|
+
await sandbox.listFiles(resolved, { includeHidden: true })
|
|
329
|
+
);
|
|
330
|
+
return createStats({ size: entries.length, type: 'directory' });
|
|
331
|
+
}
|
|
332
|
+
const info = await findChildInfo(sandbox, resolved);
|
|
333
|
+
if (info != null) {
|
|
334
|
+
return createStats({ size: info.size, type: info.type });
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const entries = normalizeFileList(
|
|
338
|
+
await sandbox.listFiles(resolved, { includeHidden: true })
|
|
339
|
+
);
|
|
340
|
+
return createStats({ size: entries.length, type: 'directory' });
|
|
341
|
+
} catch {
|
|
342
|
+
const buffer = await normalizeReadFileContent(
|
|
343
|
+
await sandbox.readFile(resolved)
|
|
344
|
+
);
|
|
345
|
+
return createStats({ size: buffer.length, type: 'file' });
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
readdir: (async (filePath: string, options?: { withFileTypes: true }) => {
|
|
349
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
350
|
+
const resolved = toSandboxPath(filePath, workspaceRoot);
|
|
351
|
+
const entries = normalizeFileList(
|
|
352
|
+
await sandbox.listFiles(resolved, { includeHidden: true })
|
|
353
|
+
);
|
|
354
|
+
if (options?.withFileTypes === true) {
|
|
355
|
+
return entries.map(createDirent);
|
|
356
|
+
}
|
|
357
|
+
return entries.map((entry) => entryNameFor(entry, resolved));
|
|
358
|
+
}) as WorkspaceFS['readdir'],
|
|
359
|
+
mkdir: async (filePath: string, options?: MakeDirectoryOptions) => {
|
|
360
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
361
|
+
await sandbox.mkdir(toSandboxPath(filePath, workspaceRoot), {
|
|
362
|
+
recursive: options?.recursive,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
realpath: async (filePath: string) =>
|
|
366
|
+
toSandboxPath(filePath, workspaceRoot),
|
|
367
|
+
unlink: async (filePath: string) => {
|
|
368
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
369
|
+
await sandbox.deleteFile(toSandboxPath(filePath, workspaceRoot));
|
|
370
|
+
},
|
|
371
|
+
open: async (filePath: string, _flags: 'r') => {
|
|
372
|
+
const sandbox = await resolveCloudflareSandbox(config);
|
|
373
|
+
const resolved = toSandboxPath(filePath, workspaceRoot);
|
|
374
|
+
const buffer = await normalizeReadFileContent(
|
|
375
|
+
await sandbox.readFile(resolved)
|
|
376
|
+
);
|
|
377
|
+
return {
|
|
378
|
+
read: async (
|
|
379
|
+
target: Buffer,
|
|
380
|
+
offset: number,
|
|
381
|
+
length: number,
|
|
382
|
+
position: number
|
|
383
|
+
) => {
|
|
384
|
+
const start = Math.max(position, 0);
|
|
385
|
+
const slice = buffer.subarray(start, start + length);
|
|
386
|
+
slice.copy(target, offset);
|
|
387
|
+
return { bytesRead: slice.length, buffer: target };
|
|
388
|
+
},
|
|
389
|
+
close: async () => undefined,
|
|
390
|
+
} as unknown as FileHandle;
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
return fs;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function createCloudflareSpawn(
|
|
398
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
399
|
+
): t.LocalSpawn {
|
|
400
|
+
return (command, args, options) => {
|
|
401
|
+
const stdout = new PassThrough();
|
|
402
|
+
const stderr = new PassThrough();
|
|
403
|
+
const abortController = new AbortController();
|
|
404
|
+
const child = new EventEmitter() as ChildProcessWithoutNullStreams;
|
|
405
|
+
const state = { closed: false };
|
|
406
|
+
const closeOnce = (
|
|
407
|
+
exitCode: number | null,
|
|
408
|
+
signal: NodeJS.Signals | null
|
|
409
|
+
): void => {
|
|
410
|
+
if (state.closed) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
state.closed = true;
|
|
414
|
+
stdout.end();
|
|
415
|
+
stderr.end();
|
|
416
|
+
Object.assign(child, {
|
|
417
|
+
exitCode,
|
|
418
|
+
signalCode: signal,
|
|
419
|
+
});
|
|
420
|
+
child.emit('close', exitCode, signal);
|
|
421
|
+
};
|
|
422
|
+
Object.assign(child, {
|
|
423
|
+
stdout,
|
|
424
|
+
stderr,
|
|
425
|
+
stdin: new PassThrough(),
|
|
426
|
+
stdio: [null, stdout, stderr],
|
|
427
|
+
killed: false,
|
|
428
|
+
exitCode: null,
|
|
429
|
+
signalCode: null,
|
|
430
|
+
pid: undefined,
|
|
431
|
+
kill: (signal: NodeJS.Signals = 'SIGTERM') => {
|
|
432
|
+
Object.assign(child, { killed: true, signalCode: signal });
|
|
433
|
+
abortController.abort();
|
|
434
|
+
closeOnce(null, signal);
|
|
435
|
+
return true;
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
void (async (): Promise<void> => {
|
|
440
|
+
const ctx = await getRuntimeContext(config);
|
|
441
|
+
const rendered = [command, ...args].map(quote).join(' ');
|
|
442
|
+
const spawnTimeoutMs = (
|
|
443
|
+
options as {
|
|
444
|
+
[LOCAL_SPAWN_TIMEOUT_MS]?: number;
|
|
445
|
+
}
|
|
446
|
+
)[LOCAL_SPAWN_TIMEOUT_MS];
|
|
447
|
+
const timeoutMs =
|
|
448
|
+
typeof spawnTimeoutMs === 'number' && Number.isFinite(spawnTimeoutMs)
|
|
449
|
+
? spawnTimeoutMs
|
|
450
|
+
: ctx.timeoutMs;
|
|
451
|
+
const timedCommand = withInSandboxTimeout(rendered, timeoutMs);
|
|
452
|
+
const cwd =
|
|
453
|
+
options.cwd == null ? ctx.workspaceRoot : options.cwd.toString();
|
|
454
|
+
try {
|
|
455
|
+
const result = await ctx.sandbox.exec(timedCommand, {
|
|
456
|
+
cwd,
|
|
457
|
+
env: ctx.env,
|
|
458
|
+
timeout: outerTimeoutMs(timeoutMs),
|
|
459
|
+
signal: abortController.signal,
|
|
460
|
+
});
|
|
461
|
+
if (state.closed) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (result.stdout) stdout.write(result.stdout);
|
|
465
|
+
if (result.stderr) stderr.write(result.stderr);
|
|
466
|
+
closeOnce(result.exitCode, null);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (state.closed) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
stderr.write((error as Error).message);
|
|
472
|
+
closeOnce(1, null);
|
|
473
|
+
}
|
|
474
|
+
})();
|
|
475
|
+
|
|
476
|
+
return child;
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function createCloudflareLocalExecutionConfig(
|
|
481
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
482
|
+
): t.LocalExecutionConfig {
|
|
483
|
+
const workspaceRoot = getCloudflareWorkspaceRoot(config);
|
|
484
|
+
return {
|
|
485
|
+
cwd: workspaceRoot,
|
|
486
|
+
workspace: { root: workspaceRoot },
|
|
487
|
+
exec: {
|
|
488
|
+
spawn: createCloudflareSpawn(config),
|
|
489
|
+
fs: createCloudflareWorkspaceFS(config),
|
|
490
|
+
sandboxed: true,
|
|
491
|
+
},
|
|
492
|
+
shell: config.shell ?? 'bash',
|
|
493
|
+
timeoutMs: config.timeoutMs,
|
|
494
|
+
maxOutputChars: config.maxOutputChars,
|
|
495
|
+
env: config.env,
|
|
496
|
+
includeCodingTools: config.includeCodingTools,
|
|
497
|
+
compileCheck: config.compileCheck,
|
|
498
|
+
readOnly: config.readOnly,
|
|
499
|
+
allowDangerousCommands: config.allowDangerousCommands,
|
|
500
|
+
bashAst: config.bashAst,
|
|
501
|
+
fileCheckpointing: config.fileCheckpointing,
|
|
502
|
+
maxReadBytes: config.maxReadBytes,
|
|
503
|
+
attachReadAttachments: config.attachReadAttachments,
|
|
504
|
+
maxAttachmentBytes: config.maxAttachmentBytes,
|
|
505
|
+
postEditSyntaxCheck: config.postEditSyntaxCheck,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function validateCloudflareBashCommand(
|
|
510
|
+
command: string,
|
|
511
|
+
args: readonly string[],
|
|
512
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
513
|
+
): Promise<void> {
|
|
514
|
+
const localConfig = createCloudflareLocalExecutionConfig(config);
|
|
515
|
+
const validation = await validateBashCommand(command, localConfig);
|
|
516
|
+
if (!validation.valid) {
|
|
517
|
+
throw new Error(validation.errors.join('\n'));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (
|
|
521
|
+
args.length > 0 &&
|
|
522
|
+
config.allowDangerousCommands !== true &&
|
|
523
|
+
DESTRUCTIVE_OP_IN_COMMAND_RE.test(command)
|
|
524
|
+
) {
|
|
525
|
+
const offending = args.find((arg) => PROTECTED_TARGET_ARG_RE.test(arg));
|
|
526
|
+
if (offending !== undefined) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`Command matches a destructive command pattern (protected target "${offending}" passed via positional arg).`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function executeCloudflareBash(
|
|
535
|
+
command: string,
|
|
536
|
+
config: t.CloudflareSandboxExecutionConfig,
|
|
537
|
+
args: readonly string[] = []
|
|
538
|
+
): Promise<SpawnResult> {
|
|
539
|
+
await validateCloudflareBashCommand(command, args, config);
|
|
540
|
+
const ctx = await getRuntimeContext(config);
|
|
541
|
+
const shellCommand =
|
|
542
|
+
args.length > 0
|
|
543
|
+
? `${ctx.shell} -lc ${quote(command)} -- ${args.map(quote).join(' ')}`
|
|
544
|
+
: `${ctx.shell} -lc ${quote(command)}`;
|
|
545
|
+
const result = await ctx.sandbox.exec(
|
|
546
|
+
withInSandboxTimeout(shellCommand, ctx.timeoutMs),
|
|
547
|
+
{
|
|
548
|
+
cwd: ctx.workspaceRoot,
|
|
549
|
+
env: ctx.env,
|
|
550
|
+
timeout: outerTimeoutMs(ctx.timeoutMs),
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
return {
|
|
554
|
+
stdout: truncateOutput(result.stdout, ctx.maxOutputChars),
|
|
555
|
+
stderr: truncateOutput(result.stderr, ctx.maxOutputChars),
|
|
556
|
+
exitCode: result.exitCode,
|
|
557
|
+
timedOut: isInSandboxTimeoutExit(result.exitCode),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function runtimeForCode(
|
|
562
|
+
lang: string,
|
|
563
|
+
tempDir: string,
|
|
564
|
+
code: string,
|
|
565
|
+
args: string[] = [],
|
|
566
|
+
shell = 'bash'
|
|
567
|
+
): RuntimeCommand {
|
|
568
|
+
const fileFor = (name: string): string => path.join(tempDir, name);
|
|
569
|
+
const argText = args.map(quote).join(' ');
|
|
570
|
+
switch (lang) {
|
|
571
|
+
case 'py':
|
|
572
|
+
case 'python':
|
|
573
|
+
return {
|
|
574
|
+
fileName: 'main.py',
|
|
575
|
+
source: code,
|
|
576
|
+
command: `python3 ${quote(fileFor('main.py'))} ${argText}`,
|
|
577
|
+
};
|
|
578
|
+
case 'js':
|
|
579
|
+
case 'javascript':
|
|
580
|
+
return {
|
|
581
|
+
fileName: 'main.js',
|
|
582
|
+
source: code,
|
|
583
|
+
command: `node ${quote(fileFor('main.js'))} ${argText}`,
|
|
584
|
+
};
|
|
585
|
+
case 'ts':
|
|
586
|
+
case 'typescript':
|
|
587
|
+
return {
|
|
588
|
+
fileName: 'main.ts',
|
|
589
|
+
source: code,
|
|
590
|
+
command: `npx --no-install tsx ${quote(fileFor('main.ts'))} ${argText}`,
|
|
591
|
+
};
|
|
592
|
+
case 'php':
|
|
593
|
+
return {
|
|
594
|
+
fileName: 'main.php',
|
|
595
|
+
source: code,
|
|
596
|
+
command: `php ${quote(fileFor('main.php'))} ${argText}`,
|
|
597
|
+
};
|
|
598
|
+
case 'go':
|
|
599
|
+
return {
|
|
600
|
+
fileName: 'main.go',
|
|
601
|
+
source: code,
|
|
602
|
+
command: `go run ${quote(fileFor('main.go'))} ${argText}`,
|
|
603
|
+
};
|
|
604
|
+
case 'rs':
|
|
605
|
+
return {
|
|
606
|
+
fileName: 'main.rs',
|
|
607
|
+
source: code,
|
|
608
|
+
command: `${shell} -lc ${quote(
|
|
609
|
+
`rustc ${quote(fileFor('main.rs'))} -o ${quote(fileFor('main-rs'))} && ${quote(fileFor('main-rs'))} ${argText}`
|
|
610
|
+
)}`,
|
|
611
|
+
};
|
|
612
|
+
case 'c':
|
|
613
|
+
return {
|
|
614
|
+
fileName: 'main.c',
|
|
615
|
+
source: code,
|
|
616
|
+
command: `${shell} -lc ${quote(
|
|
617
|
+
`cc ${quote(fileFor('main.c'))} -o ${quote(fileFor('main-c'))} && ${quote(fileFor('main-c'))} ${argText}`
|
|
618
|
+
)}`,
|
|
619
|
+
};
|
|
620
|
+
case 'cpp':
|
|
621
|
+
return {
|
|
622
|
+
fileName: 'main.cpp',
|
|
623
|
+
source: code,
|
|
624
|
+
command: `${shell} -lc ${quote(
|
|
625
|
+
`c++ ${quote(fileFor('main.cpp'))} -o ${quote(fileFor('main-cpp'))} && ${quote(fileFor('main-cpp'))} ${argText}`
|
|
626
|
+
)}`,
|
|
627
|
+
};
|
|
628
|
+
case 'java':
|
|
629
|
+
return {
|
|
630
|
+
fileName: 'Main.java',
|
|
631
|
+
source: code,
|
|
632
|
+
command: `${shell} -lc ${quote(
|
|
633
|
+
`javac ${quote(fileFor('Main.java'))} && java -cp ${quote(tempDir)} Main ${argText}`
|
|
634
|
+
)}`,
|
|
635
|
+
};
|
|
636
|
+
case 'r':
|
|
637
|
+
return {
|
|
638
|
+
fileName: 'main.R',
|
|
639
|
+
source: code,
|
|
640
|
+
command: `Rscript ${quote(fileFor('main.R'))} ${argText}`,
|
|
641
|
+
};
|
|
642
|
+
case 'd':
|
|
643
|
+
return {
|
|
644
|
+
fileName: 'main.d',
|
|
645
|
+
source: code,
|
|
646
|
+
command: `${shell} -lc ${quote(
|
|
647
|
+
`dmd ${quote(fileFor('main.d'))} -of=${quote(fileFor('main-d'))} && ${quote(fileFor('main-d'))} ${argText}`
|
|
648
|
+
)}`,
|
|
649
|
+
};
|
|
650
|
+
case 'f90':
|
|
651
|
+
return {
|
|
652
|
+
fileName: 'main.f90',
|
|
653
|
+
source: code,
|
|
654
|
+
command: `${shell} -lc ${quote(
|
|
655
|
+
`gfortran ${quote(fileFor('main.f90'))} -o ${quote(fileFor('main-f90'))} && ${quote(fileFor('main-f90'))} ${argText}`
|
|
656
|
+
)}`,
|
|
657
|
+
};
|
|
658
|
+
case 'bash':
|
|
659
|
+
case 'sh':
|
|
660
|
+
return {
|
|
661
|
+
fileName: 'main.sh',
|
|
662
|
+
source: code,
|
|
663
|
+
command: `${shell} -lc ${quote(code)} -- ${argText}`,
|
|
664
|
+
};
|
|
665
|
+
default:
|
|
666
|
+
throw new Error(`Unsupported Cloudflare sandbox runtime: ${lang}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export async function executeCloudflareCode(
|
|
671
|
+
input: { lang: string; code: string; args?: string[] },
|
|
672
|
+
config: t.CloudflareSandboxExecutionConfig
|
|
673
|
+
): Promise<SpawnResult> {
|
|
674
|
+
if (input.lang === 'bash' || input.lang === 'sh') {
|
|
675
|
+
return executeCloudflareBash(input.code, config, input.args ?? []);
|
|
676
|
+
}
|
|
677
|
+
const ctx = await getRuntimeContext(config);
|
|
678
|
+
const id = globalThis.crypto.randomUUID();
|
|
679
|
+
const tempDir = path.join(ctx.workspaceRoot, '.lc-exec', id);
|
|
680
|
+
const runtime = runtimeForCode(
|
|
681
|
+
input.lang,
|
|
682
|
+
tempDir,
|
|
683
|
+
input.code,
|
|
684
|
+
input.args,
|
|
685
|
+
ctx.shell
|
|
686
|
+
);
|
|
687
|
+
await ctx.sandbox.mkdir(tempDir, { recursive: true });
|
|
688
|
+
if (runtime.source != null) {
|
|
689
|
+
await ctx.sandbox.writeFile(
|
|
690
|
+
path.join(tempDir, runtime.fileName),
|
|
691
|
+
runtime.source,
|
|
692
|
+
{
|
|
693
|
+
encoding: 'utf8',
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const result = await ctx.sandbox.exec(
|
|
699
|
+
withInSandboxTimeout(runtime.command, ctx.timeoutMs),
|
|
700
|
+
{
|
|
701
|
+
cwd: ctx.workspaceRoot,
|
|
702
|
+
env: ctx.env,
|
|
703
|
+
timeout: outerTimeoutMs(ctx.timeoutMs),
|
|
704
|
+
}
|
|
705
|
+
);
|
|
706
|
+
return {
|
|
707
|
+
stdout: truncateOutput(result.stdout, ctx.maxOutputChars),
|
|
708
|
+
stderr: truncateOutput(result.stderr, ctx.maxOutputChars),
|
|
709
|
+
exitCode: result.exitCode,
|
|
710
|
+
timedOut: isInSandboxTimeoutExit(result.exitCode),
|
|
711
|
+
};
|
|
712
|
+
} finally {
|
|
713
|
+
await ctx.sandbox
|
|
714
|
+
.exec(`rm -rf ${quote(tempDir)}`, {
|
|
715
|
+
cwd: ctx.workspaceRoot,
|
|
716
|
+
env: ctx.env,
|
|
717
|
+
timeout: 10000,
|
|
718
|
+
})
|
|
719
|
+
.catch(() => undefined);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function formatCloudflareOutput(
|
|
724
|
+
result: SpawnResult,
|
|
725
|
+
cwd: string
|
|
726
|
+
): string {
|
|
727
|
+
let formatted = '';
|
|
728
|
+
if (result.stdout !== '') {
|
|
729
|
+
formatted += `stdout:\n${result.stdout}\n`;
|
|
730
|
+
} else {
|
|
731
|
+
formatted += 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
|
|
732
|
+
}
|
|
733
|
+
if (result.stderr !== '') {
|
|
734
|
+
formatted += `stderr:\n${result.stderr}\n`;
|
|
735
|
+
}
|
|
736
|
+
if (result.exitCode != null && result.exitCode !== 0) {
|
|
737
|
+
formatted += `exit_code: ${result.exitCode}\n`;
|
|
738
|
+
}
|
|
739
|
+
if (result.timedOut) {
|
|
740
|
+
formatted += 'timed_out: true\n';
|
|
741
|
+
}
|
|
742
|
+
formatted += `working_directory: ${cwd}`;
|
|
743
|
+
return formatted.trim();
|
|
744
|
+
}
|