@librechat/agents 3.1.90 → 3.1.92

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 (94) 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 +48 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/instrumentation.cjs +2 -7
  6. package/dist/cjs/instrumentation.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +285 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +25 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +75 -44
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/stream.cjs +10 -3
  14. package/dist/cjs/stream.cjs.map +1 -1
  15. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  16. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  17. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  18. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  19. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  20. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  21. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  22. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  23. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  24. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  25. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  26. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  27. package/dist/cjs/utils/callbacks.cjs +27 -0
  28. package/dist/cjs/utils/callbacks.cjs.map +1 -0
  29. package/dist/esm/agents/AgentContext.mjs +9 -5
  30. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +48 -14
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/instrumentation.mjs +2 -7
  34. package/dist/esm/instrumentation.mjs.map +1 -1
  35. package/dist/esm/langfuse.mjs +275 -0
  36. package/dist/esm/langfuse.mjs.map +1 -0
  37. package/dist/esm/main.mjs +5 -1
  38. package/dist/esm/main.mjs.map +1 -1
  39. package/dist/esm/run.mjs +75 -44
  40. package/dist/esm/run.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +10 -3
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  44. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  45. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  46. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  47. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  48. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  49. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  50. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  51. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  52. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  53. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  54. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  55. package/dist/esm/utils/callbacks.mjs +24 -0
  56. package/dist/esm/utils/callbacks.mjs.map +1 -0
  57. package/dist/types/agents/AgentContext.d.ts +4 -1
  58. package/dist/types/graphs/Graph.d.ts +6 -5
  59. package/dist/types/index.d.ts +1 -0
  60. package/dist/types/langfuse.d.ts +57 -0
  61. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  62. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  63. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  64. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  65. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  66. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  67. package/dist/types/types/graph.d.ts +8 -0
  68. package/dist/types/types/run.d.ts +2 -2
  69. package/dist/types/types/tools.d.ts +118 -2
  70. package/dist/types/utils/callbacks.d.ts +5 -0
  71. package/package.json +4 -4
  72. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  73. package/src/agents/AgentContext.ts +13 -3
  74. package/src/graphs/Graph.ts +57 -16
  75. package/src/index.ts +1 -0
  76. package/src/instrumentation.ts +2 -7
  77. package/src/langfuse.ts +441 -0
  78. package/src/run.ts +105 -59
  79. package/src/specs/langfuse-callbacks.test.ts +75 -0
  80. package/src/specs/langfuse-config.test.ts +114 -0
  81. package/src/specs/langfuse-metadata.test.ts +19 -1
  82. package/src/stream.ts +13 -3
  83. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  84. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  85. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  86. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  87. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  88. package/src/tools/cloudflare/index.ts +4 -0
  89. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  90. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  91. package/src/types/graph.ts +9 -0
  92. package/src/types/run.ts +2 -7
  93. package/src/types/tools.ts +141 -2
  94. package/src/utils/callbacks.ts +39 -0
@@ -0,0 +1,537 @@
1
+ import type * as t from '@/types';
2
+ import { Constants } from '@/common';
3
+ import { spawnLocalProcess } from '../local/LocalExecutionEngine';
4
+ import { resolveLocalToolsForBinding } from '../local/resolveLocalExecutionTools';
5
+ import {
6
+ createCloudflareWorkspaceFS,
7
+ createCloudflareLocalExecutionConfig,
8
+ executeCloudflareBash,
9
+ executeCloudflareCode,
10
+ } from '../cloudflare/CloudflareSandboxExecutionEngine';
11
+ import { createCloudflareBridgeRuntime } from '../cloudflare/CloudflareBridgeRuntime';
12
+ import {
13
+ createCloudflareBashProgrammaticToolCallingTool,
14
+ createCloudflareProgrammaticToolCallingTool,
15
+ } from '../cloudflare/CloudflareProgrammaticToolCalling';
16
+
17
+ function sseResponse(events: string): Response {
18
+ return new Response(events, {
19
+ status: 200,
20
+ headers: { 'Content-Type': 'text/event-stream' },
21
+ });
22
+ }
23
+
24
+ function sseExit(exitCode = 0): Response {
25
+ return sseResponse(`event: exit\ndata: {"exit_code":${exitCode}}\n\n`);
26
+ }
27
+
28
+ function bodyText(body: BodyInit | null | undefined): string {
29
+ if (typeof body === 'string') {
30
+ return body;
31
+ }
32
+ if (body == null) {
33
+ return '';
34
+ }
35
+ if (body instanceof Uint8Array) {
36
+ return Buffer.from(body).toString('utf8');
37
+ }
38
+ return String(body);
39
+ }
40
+
41
+ function createRuntime(
42
+ overrides: Partial<t.CloudflareSandboxRuntime> = {}
43
+ ): t.CloudflareSandboxRuntime {
44
+ return {
45
+ exec: async () => ({
46
+ exitCode: 0,
47
+ stdout: '',
48
+ stderr: '',
49
+ }),
50
+ readFile: async () => '',
51
+ writeFile: async () => ({ ok: true }),
52
+ mkdir: async () => ({ ok: true }),
53
+ listFiles: async () => [],
54
+ deleteFile: async () => ({ ok: true }),
55
+ ...overrides,
56
+ };
57
+ }
58
+
59
+ describe('Cloudflare sandbox execution backend', () => {
60
+ it('normalizes trailing workspace slashes before clamping paths', async () => {
61
+ const readPaths: string[] = [];
62
+ const fs = createCloudflareWorkspaceFS({
63
+ workspaceRoot: '/workspace/',
64
+ sandbox: createRuntime({
65
+ readFile: async (filePath) => {
66
+ readPaths.push(filePath);
67
+ return 'ok';
68
+ },
69
+ }),
70
+ });
71
+
72
+ await expect(fs.readFile('file.txt', 'utf8')).resolves.toBe('ok');
73
+ expect(readPaths).toEqual(['/workspace/file.txt']);
74
+ });
75
+
76
+ it('allows root workspace paths', async () => {
77
+ const readPaths: string[] = [];
78
+ const fs = createCloudflareWorkspaceFS({
79
+ workspaceRoot: '/',
80
+ sandbox: createRuntime({
81
+ readFile: async (filePath) => {
82
+ readPaths.push(filePath);
83
+ return 'ok';
84
+ },
85
+ }),
86
+ });
87
+
88
+ await expect(fs.readFile('tmp/file.txt', 'utf8')).resolves.toBe('ok');
89
+ expect(readPaths).toEqual(['/tmp/file.txt']);
90
+ });
91
+
92
+ it('stats the workspace root without listing its parent directory', async () => {
93
+ const listPaths: string[] = [];
94
+ const fs = createCloudflareWorkspaceFS({
95
+ workspaceRoot: '/workspace/',
96
+ sandbox: createRuntime({
97
+ listFiles: async (filePath) => {
98
+ listPaths.push(filePath);
99
+ return [{ name: 'src', type: 'directory' }];
100
+ },
101
+ }),
102
+ });
103
+
104
+ const stats = await fs.stat('.');
105
+
106
+ expect(stats.isDirectory()).toBe(true);
107
+ expect(listPaths).toEqual(['/workspace']);
108
+ });
109
+
110
+ it('aborts remote exec when the local timeout kills the spawn wrapper', async () => {
111
+ let signal: AbortSignal | undefined;
112
+ const sandbox = createRuntime({
113
+ exec: (_command, options) =>
114
+ new Promise<t.CloudflareSandboxExecResult>((_resolve, reject) => {
115
+ signal = options?.signal;
116
+ signal?.addEventListener('abort', () => reject(new Error('aborted')));
117
+ }),
118
+ });
119
+ const config = createCloudflareLocalExecutionConfig({
120
+ sandbox,
121
+ timeoutMs: 10,
122
+ workspaceRoot: '/workspace',
123
+ });
124
+
125
+ const result = await spawnLocalProcess('bash', ['-lc', 'sleep 10'], config);
126
+
127
+ expect(signal?.aborted).toBe(true);
128
+ expect(result.timedOut).toBe(true);
129
+ expect(result.exitCode).toBe(143);
130
+ });
131
+
132
+ it('memoizes sandbox factory results per config object', async () => {
133
+ let calls = 0;
134
+ const runtime = createRuntime({
135
+ readFile: async () => 'ok',
136
+ writeFile: async () => ({ ok: true }),
137
+ });
138
+ const config = {
139
+ workspaceRoot: '/workspace',
140
+ sandbox: async (): Promise<t.CloudflareSandboxRuntime> => {
141
+ calls += 1;
142
+ return runtime;
143
+ },
144
+ };
145
+ const fs = createCloudflareWorkspaceFS(config);
146
+
147
+ await fs.readFile('a.txt', 'utf8');
148
+ await fs.writeFile('b.txt', 'ok', 'utf8');
149
+
150
+ expect(calls).toBe(1);
151
+ });
152
+
153
+ it('wraps direct bash commands with an in-sandbox timeout', async () => {
154
+ let execCommand = '';
155
+ let execTimeout: number | undefined;
156
+ let calls = 0;
157
+ const sandbox = createRuntime({
158
+ exec: async (command, options) => {
159
+ calls += 1;
160
+ execCommand = command;
161
+ execTimeout = options?.timeout;
162
+ return {
163
+ exitCode: calls === 1 ? 0 : 124,
164
+ stdout: 'ok',
165
+ stderr: '',
166
+ };
167
+ },
168
+ });
169
+
170
+ const result = await executeCloudflareBash('echo ok', {
171
+ sandbox,
172
+ workspaceRoot: '/workspace',
173
+ timeoutMs: 1500,
174
+ });
175
+
176
+ expect(execCommand).toContain('timeout -k 2s 2s bash -lc');
177
+ expect(execTimeout).toBe(6500);
178
+ expect(result.timedOut).toBe(true);
179
+ });
180
+
181
+ it('passes call-specific timeouts to the Cloudflare spawn wrapper', async () => {
182
+ let execCommand = '';
183
+ let execTimeout: number | undefined;
184
+ const sandbox = createRuntime({
185
+ exec: async (command, options) => {
186
+ execCommand = command;
187
+ execTimeout = options?.timeout;
188
+ return {
189
+ exitCode: 0,
190
+ stdout: 'ok',
191
+ stderr: '',
192
+ };
193
+ },
194
+ });
195
+ const config = createCloudflareLocalExecutionConfig({
196
+ sandbox,
197
+ workspaceRoot: '/workspace',
198
+ timeoutMs: 1000,
199
+ });
200
+
201
+ await expect(
202
+ spawnLocalProcess('bash', ['-lc', 'echo ok'], {
203
+ ...config,
204
+ timeoutMs: 120000,
205
+ })
206
+ ).resolves.toMatchObject({ exitCode: 0, timedOut: false });
207
+
208
+ expect(execCommand).toContain('timeout -k 2s 120s bash -lc');
209
+ expect(execTimeout).toBe(125000);
210
+ });
211
+
212
+ it('marks Cloudflare code execution timeouts', async () => {
213
+ const sandbox = createRuntime({
214
+ exec: async (command) => ({
215
+ exitCode: command.startsWith('rm -rf') ? 0 : 124,
216
+ stdout: '',
217
+ stderr: '',
218
+ }),
219
+ });
220
+
221
+ const result = await executeCloudflareCode(
222
+ { lang: 'py', code: 'print("slow")' },
223
+ {
224
+ sandbox,
225
+ workspaceRoot: '/workspace',
226
+ timeoutMs: 1000,
227
+ }
228
+ );
229
+
230
+ expect(result.timedOut).toBe(true);
231
+ });
232
+
233
+ it('forwards only explicit Cloudflare env vars to sandbox exec', async () => {
234
+ let execEnv: Record<string, string | undefined> | undefined;
235
+ const sandbox = createRuntime({
236
+ exec: async (_command, options) => {
237
+ execEnv = options?.env;
238
+ return {
239
+ exitCode: 0,
240
+ stdout: 'ok',
241
+ stderr: '',
242
+ };
243
+ },
244
+ });
245
+ const config = createCloudflareLocalExecutionConfig({
246
+ sandbox,
247
+ workspaceRoot: '/workspace',
248
+ env: { SAFE_FOR_SANDBOX: 'yes' },
249
+ });
250
+
251
+ await spawnLocalProcess('bash', ['-lc', 'echo ok'], config);
252
+
253
+ expect(execEnv).toEqual({ SAFE_FOR_SANDBOX: 'yes' });
254
+ });
255
+
256
+ it('injects read-only and workspace guards into Python programmatic tools', async () => {
257
+ let source = '';
258
+ const sandbox = createRuntime({
259
+ writeFile: async (_path, content) => {
260
+ source = String(content);
261
+ return { ok: true };
262
+ },
263
+ exec: async () => ({
264
+ exitCode: 0,
265
+ stdout: 'done',
266
+ stderr: '',
267
+ }),
268
+ });
269
+ const programmatic = createCloudflareProgrammaticToolCallingTool({
270
+ sandbox,
271
+ workspaceRoot: '/workspace',
272
+ readOnly: true,
273
+ shell: '/bin/sh',
274
+ });
275
+
276
+ await programmatic.invoke({
277
+ code: 'await write_file("x.txt", "blocked")',
278
+ lang: 'py',
279
+ });
280
+
281
+ expect(source).toContain('READ_ONLY = True');
282
+ expect(source).toContain('SHELL = "/bin/sh"');
283
+ expect(source).toContain('[SHELL, "-lc", command, "--"]');
284
+ expect(source).toContain('_assert_writable("write_file")');
285
+ expect(source).toContain('if _is_within_workspace(resolved):');
286
+ expect(source).toContain('_validate_bash_command(command, args=args)');
287
+ });
288
+
289
+ it('clamps programmatic timeouts before sandbox execution', async () => {
290
+ const timeouts: Array<number | undefined> = [];
291
+ const sandbox = createRuntime({
292
+ writeFile: async () => ({ ok: true }),
293
+ exec: async (_command, options) => {
294
+ timeouts.push(options?.timeout);
295
+ return {
296
+ exitCode: 0,
297
+ stdout: 'done',
298
+ stderr: '',
299
+ };
300
+ },
301
+ });
302
+ const programmatic = createCloudflareProgrammaticToolCallingTool({
303
+ sandbox,
304
+ workspaceRoot: '/workspace',
305
+ });
306
+
307
+ await programmatic.invoke({
308
+ code: 'print("ok")',
309
+ lang: 'py',
310
+ timeout: 300000,
311
+ });
312
+
313
+ expect(timeouts[0]).toBe(305000);
314
+ });
315
+
316
+ it('injects bash validation into bash programmatic tools', async () => {
317
+ let execCommand = '';
318
+ const sandbox = createRuntime({
319
+ exec: async (command) => {
320
+ execCommand = command;
321
+ return {
322
+ exitCode: 0,
323
+ stdout: 'done',
324
+ stderr: '',
325
+ };
326
+ },
327
+ });
328
+ const programmatic = createCloudflareBashProgrammaticToolCallingTool({
329
+ sandbox,
330
+ workspaceRoot: '/workspace',
331
+ shell: '/bin/sh',
332
+ });
333
+
334
+ await programmatic.invoke({
335
+ code: 'printf "%s\\n" "ok"',
336
+ });
337
+
338
+ expect(execCommand).toContain('const ALLOW_DANGEROUS_COMMANDS = false;');
339
+ expect(execCommand).toContain('const SHELL = "/bin/sh";');
340
+ expect(execCommand).toContain('cp.spawn(SHELL, ["-lc", command, "--"');
341
+ expect(execCommand).toContain(
342
+ 'function validateBashCommand(command, args)'
343
+ );
344
+ expect(execCommand).toContain('validateBashCommand(command, args);');
345
+ });
346
+
347
+ it('uses root-safe path containment in programmatic helpers', async () => {
348
+ let pythonSource = '';
349
+ const pythonSandbox = createRuntime({
350
+ writeFile: async (_path, content) => {
351
+ pythonSource = String(content);
352
+ return { ok: true };
353
+ },
354
+ exec: async () => ({
355
+ exitCode: 0,
356
+ stdout: 'done',
357
+ stderr: '',
358
+ }),
359
+ });
360
+ const pythonProgrammatic = createCloudflareProgrammaticToolCallingTool({
361
+ sandbox: pythonSandbox,
362
+ workspaceRoot: '/',
363
+ });
364
+
365
+ await pythonProgrammatic.invoke({
366
+ code: 'print("ok")',
367
+ lang: 'py',
368
+ });
369
+
370
+ expect(pythonSource).toContain('WORKSPACE = "/"');
371
+ expect(pythonSource).toContain(
372
+ 'return os.path.commonpath([root, resolved]) == root'
373
+ );
374
+
375
+ let bashCommand = '';
376
+ const bashSandbox = createRuntime({
377
+ exec: async (command) => {
378
+ bashCommand = command;
379
+ return {
380
+ exitCode: 0,
381
+ stdout: 'done',
382
+ stderr: '',
383
+ };
384
+ },
385
+ });
386
+ const bashProgrammatic = createCloudflareBashProgrammaticToolCallingTool({
387
+ sandbox: bashSandbox,
388
+ workspaceRoot: '/',
389
+ });
390
+
391
+ await bashProgrammatic.invoke({
392
+ code: 'printf "%s\\n" "ok"',
393
+ });
394
+
395
+ expect(bashCommand).toContain('const WORKSPACE = "/";');
396
+ expect(bashCommand).toContain(
397
+ 'const relative = path.relative(root, resolved);'
398
+ );
399
+ expect(bashCommand).toContain(
400
+ 'relative.startsWith("..") || path.isAbsolute(relative)'
401
+ );
402
+ });
403
+
404
+ it('enforces Cloudflare codingToolNames as an allowlist', () => {
405
+ const tools = resolveLocalToolsForBinding({
406
+ tools: [
407
+ { name: Constants.EXECUTE_CODE } as t.GenericTool,
408
+ { name: Constants.BASH_TOOL } as t.GenericTool,
409
+ ],
410
+ toolExecution: {
411
+ engine: 'cloudflare-sandbox',
412
+ cloudflare: {
413
+ sandbox: createRuntime(),
414
+ codingToolNames: [Constants.BASH_TOOL],
415
+ },
416
+ },
417
+ }) as t.GenericTool[];
418
+ const names = tools.map((toolDef) => toolDef.name);
419
+
420
+ expect(names).toContain(Constants.BASH_TOOL);
421
+ expect(names).not.toContain(Constants.EXECUTE_CODE);
422
+ });
423
+ });
424
+
425
+ describe('Cloudflare bridge runtime', () => {
426
+ it('preserves caller-provided sandbox ids', async () => {
427
+ const urls: string[] = [];
428
+ const fetchImpl: typeof fetch = async (input) => {
429
+ urls.push(input.toString());
430
+ if (input.toString().endsWith('/exec')) {
431
+ return sseExit();
432
+ }
433
+ throw new Error(`Unexpected URL: ${input.toString()}`);
434
+ };
435
+ const runtime = createCloudflareBridgeRuntime({
436
+ baseURL: 'https://bridge.example',
437
+ sandboxId: 'user-123',
438
+ fetch: fetchImpl,
439
+ });
440
+
441
+ await expect(runtime.getSandboxId()).resolves.toBe('user-123');
442
+ await runtime.exec('true');
443
+
444
+ expect(urls).toEqual(['https://bridge.example/v1/sandbox/user-123/exec']);
445
+ });
446
+
447
+ it('retries sandbox creation after a transient create failure', async () => {
448
+ let createCalls = 0;
449
+ const fetchImpl: typeof fetch = async (input) => {
450
+ const url = input.toString();
451
+ if (url.endsWith('/sandbox')) {
452
+ createCalls += 1;
453
+ if (createCalls === 1) {
454
+ return new Response('try again', { status: 503 });
455
+ }
456
+ return Response.json({ id: 'retryid' });
457
+ }
458
+ throw new Error(`Unexpected URL: ${url}`);
459
+ };
460
+ const runtime = createCloudflareBridgeRuntime({
461
+ baseURL: 'https://bridge.example',
462
+ fetch: fetchImpl,
463
+ });
464
+
465
+ await expect(runtime.getSandboxId()).rejects.toThrow('503');
466
+ await expect(runtime.getSandboxId()).resolves.toBe('retryid');
467
+ expect(createCalls).toBe(2);
468
+ });
469
+
470
+ it('fails exec when the SSE stream ends before an exit event', async () => {
471
+ const fetchImpl: typeof fetch = async (input) => {
472
+ const url = input.toString();
473
+ if (url.endsWith('/exec')) {
474
+ const stdout = Buffer.from('partial').toString('base64');
475
+ return sseResponse(`event: stdout\ndata: ${stdout}\n\n`);
476
+ }
477
+ throw new Error(`Unexpected URL: ${url}`);
478
+ };
479
+ const runtime = createCloudflareBridgeRuntime({
480
+ baseURL: 'https://bridge.example',
481
+ sandboxId: 'abc',
482
+ fetch: fetchImpl,
483
+ });
484
+
485
+ await expect(runtime.exec('echo partial')).rejects.toThrow(
486
+ 'closed before an exit event'
487
+ );
488
+ });
489
+
490
+ it('prunes hidden directory trees when includeHidden is disabled', async () => {
491
+ let command = '';
492
+ const fetchImpl: typeof fetch = async (input, init) => {
493
+ const url = input.toString();
494
+ if (url.endsWith('/exec')) {
495
+ const body = JSON.parse(bodyText(init?.body)) as { argv?: string[] };
496
+ command = body.argv?.[2] ?? '';
497
+ return sseExit();
498
+ }
499
+ throw new Error(`Unexpected URL: ${url}`);
500
+ };
501
+ const runtime = createCloudflareBridgeRuntime({
502
+ baseURL: 'https://bridge.example',
503
+ sandboxId: 'abc',
504
+ fetch: fetchImpl,
505
+ });
506
+
507
+ await runtime.listFiles('/workspace', {
508
+ recursive: true,
509
+ includeHidden: false,
510
+ });
511
+
512
+ expect(command).toContain('-prune');
513
+ expect(command).toContain('\\( -name \'.*\' -prune \\) -o');
514
+ });
515
+
516
+ it('fails bridge listFiles for non-directory targets', async () => {
517
+ const fetchImpl: typeof fetch = async (input) => {
518
+ const url = input.toString();
519
+ if (url.endsWith('/exec')) {
520
+ const stderr = Buffer.from('not a directory').toString('base64');
521
+ return sseResponse(
522
+ `event: stderr\ndata: ${stderr}\n\nevent: exit\ndata: {"exit_code":20}\n\n`
523
+ );
524
+ }
525
+ throw new Error(`Unexpected URL: ${url}`);
526
+ };
527
+ const runtime = createCloudflareBridgeRuntime({
528
+ baseURL: 'https://bridge.example',
529
+ sandboxId: 'abc',
530
+ fetch: fetchImpl,
531
+ });
532
+
533
+ await expect(runtime.listFiles('/workspace/file.txt')).rejects.toThrow(
534
+ 'not a directory'
535
+ );
536
+ });
537
+ });