@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.
Files changed (80) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +9 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +46 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +234 -0
  6. package/dist/cjs/langfuse.cjs.map +1 -0
  7. package/dist/cjs/main.cjs +25 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +44 -27
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +10 -3
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  14. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  15. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  16. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  17. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  18. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  19. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  20. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  21. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  22. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  23. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  24. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  25. package/dist/esm/agents/AgentContext.mjs +9 -5
  26. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  27. package/dist/esm/graphs/Graph.mjs +46 -14
  28. package/dist/esm/graphs/Graph.mjs.map +1 -1
  29. package/dist/esm/langfuse.mjs +226 -0
  30. package/dist/esm/langfuse.mjs.map +1 -0
  31. package/dist/esm/main.mjs +5 -1
  32. package/dist/esm/main.mjs.map +1 -1
  33. package/dist/esm/run.mjs +44 -27
  34. package/dist/esm/run.mjs.map +1 -1
  35. package/dist/esm/stream.mjs +10 -3
  36. package/dist/esm/stream.mjs.map +1 -1
  37. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  38. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  39. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  40. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  41. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  42. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  43. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  44. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  45. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  46. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  47. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  48. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  49. package/dist/types/agents/AgentContext.d.ts +4 -1
  50. package/dist/types/graphs/Graph.d.ts +6 -5
  51. package/dist/types/index.d.ts +1 -0
  52. package/dist/types/langfuse.d.ts +48 -0
  53. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  54. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  55. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  56. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  57. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  58. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  59. package/dist/types/types/graph.d.ts +8 -0
  60. package/dist/types/types/tools.d.ts +118 -2
  61. package/package.json +4 -4
  62. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  63. package/src/agents/AgentContext.ts +13 -3
  64. package/src/graphs/Graph.ts +53 -16
  65. package/src/index.ts +1 -0
  66. package/src/langfuse.ts +358 -0
  67. package/src/run.ts +60 -38
  68. package/src/specs/langfuse-config.test.ts +57 -0
  69. package/src/specs/langfuse-metadata.test.ts +19 -1
  70. package/src/stream.ts +13 -3
  71. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  72. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  73. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  74. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  75. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  76. package/src/tools/cloudflare/index.ts +4 -0
  77. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  78. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  79. package/src/types/graph.ts +9 -0
  80. 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
+ }