@librechat/agents 3.2.39 → 3.2.42

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 (59) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +4 -1
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
  5. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
  6. package/dist/cjs/llm/bedrock/index.cjs +9 -1
  7. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  8. package/dist/cjs/main.cjs +4 -0
  9. package/dist/cjs/messages/cache.cjs +21 -0
  10. package/dist/cjs/messages/cache.cjs.map +1 -1
  11. package/dist/cjs/tools/ReadFile.cjs +2 -2
  12. package/dist/cjs/tools/ReadFile.cjs.map +1 -1
  13. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +13 -13
  14. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +87 -7
  16. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -1
  17. package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
  18. package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
  19. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +5 -2
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
  23. package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
  24. package/dist/esm/llm/bedrock/index.mjs +10 -2
  25. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  26. package/dist/esm/main.mjs +3 -3
  27. package/dist/esm/messages/cache.mjs +21 -1
  28. package/dist/esm/messages/cache.mjs.map +1 -1
  29. package/dist/esm/tools/ReadFile.mjs +2 -2
  30. package/dist/esm/tools/ReadFile.mjs.map +1 -1
  31. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +14 -14
  32. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  33. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +85 -8
  34. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -1
  35. package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
  36. package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
  37. package/dist/types/llm/bedrock/index.d.ts +7 -0
  38. package/dist/types/messages/cache.d.ts +17 -0
  39. package/dist/types/tools/ReadFile.d.ts +4 -4
  40. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +44 -0
  41. package/package.json +1 -1
  42. package/src/agents/AgentContext.ts +2 -0
  43. package/src/graphs/Graph.ts +17 -5
  44. package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
  45. package/src/hooks/createWorkspacePolicyHook.ts +7 -6
  46. package/src/llm/bedrock/index.ts +18 -2
  47. package/src/llm/bedrock/llm.spec.ts +97 -0
  48. package/src/messages/cache.test.ts +31 -0
  49. package/src/messages/cache.ts +25 -0
  50. package/src/tools/ReadFile.ts +2 -2
  51. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +131 -0
  52. package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
  53. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
  54. package/src/tools/__tests__/ReadFile.test.ts +3 -3
  55. package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
  56. package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
  57. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +18 -13
  58. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +165 -9
  59. package/src/tools/local/LocalCodingTools.ts +14 -14
@@ -2,6 +2,7 @@ import type * as t from '@/types';
2
2
  import {
3
3
  createCloudflareWorkspaceFS,
4
4
  createCloudflareLocalExecutionConfig,
5
+ execWithClientTimeout,
5
6
  executeCloudflareBash,
6
7
  executeCloudflareCode,
7
8
  } from '../cloudflare/CloudflareSandboxExecutionEngine';
@@ -257,6 +258,136 @@ describe('Cloudflare sandbox execution backend', () => {
257
258
  expect(result.timedOut).toBe(true);
258
259
  });
259
260
 
261
+ it('rejects with a client-side timeout when sandbox exec stalls (no native cancellation)', async () => {
262
+ // The native Cloudflare Sandbox DO exec() is uncancellable (ExecOptions has no
263
+ // signal) and its own `timeout` is not enforced when the container/RPC stalls,
264
+ // while the in-sandbox `timeout(1)` wrapper only bounds a *running* command.
265
+ // Without a client-side race a stalled exec hangs until the host's run-level
266
+ // abort, burning the whole budget on one tool call (issue #251).
267
+ jest.useFakeTimers();
268
+ try {
269
+ let mainExecCalls = 0;
270
+ const sandbox = createRuntime({
271
+ exec: (command) => {
272
+ // Cleanup (`rm -rf`) resolves immediately; the real command stalls,
273
+ // simulating an unresponsive / cold container exec that never returns.
274
+ if (command.startsWith('rm -rf')) {
275
+ return Promise.resolve({ exitCode: 0, stdout: '', stderr: '' });
276
+ }
277
+ mainExecCalls += 1;
278
+ return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
279
+ },
280
+ });
281
+
282
+ const promise = executeCloudflareCode(
283
+ { lang: 'py', code: 'print("slow")' },
284
+ { sandbox, workspaceRoot: '/workspace', timeoutMs: 1000 }
285
+ );
286
+ const assertion = expect(promise).rejects.toThrow(/client-side timeout/);
287
+
288
+ // Client backstop = outerTimeoutMs(1000) + 5000 = 11000ms; advance past it.
289
+ await jest.advanceTimersByTimeAsync(11500);
290
+ await assertion;
291
+ expect(mainExecCalls).toBe(1);
292
+ } finally {
293
+ jest.useRealTimers();
294
+ }
295
+ });
296
+
297
+ it('aborts signal-aware execs when the client timeout fires', async () => {
298
+ // For signal-aware transports (e.g. the HTTP bridge), a client timeout should
299
+ // actually cancel the underlying exec, not just abandon it.
300
+ jest.useFakeTimers();
301
+ try {
302
+ let mainSignal: AbortSignal | undefined;
303
+ const sandbox = createRuntime({
304
+ supportsExecSignal: true,
305
+ exec: (command, options) => {
306
+ if (command.startsWith('rm -rf')) {
307
+ return Promise.resolve({ exitCode: 0, stdout: '', stderr: '' });
308
+ }
309
+ mainSignal = options?.signal;
310
+ return new Promise<t.CloudflareSandboxExecResult>(() => undefined);
311
+ },
312
+ });
313
+
314
+ const promise = executeCloudflareCode(
315
+ { lang: 'py', code: 'print("slow")' },
316
+ { sandbox, workspaceRoot: '/workspace', timeoutMs: 1000 }
317
+ );
318
+ const assertion = expect(promise).rejects.toThrow(/client-side timeout/);
319
+ await jest.advanceTimersByTimeAsync(11500);
320
+ await assertion;
321
+
322
+ expect(mainSignal).toBeDefined();
323
+ expect(mainSignal?.aborted).toBe(true);
324
+ } finally {
325
+ jest.useRealTimers();
326
+ }
327
+ });
328
+
329
+ it('composes a caller abort signal with the timeout instead of clobbering it', async () => {
330
+ let received: AbortSignal | undefined;
331
+ const sandbox = createRuntime({
332
+ supportsExecSignal: true,
333
+ exec: (_command, options) => {
334
+ received = options?.signal;
335
+ return new Promise<t.CloudflareSandboxExecResult>(
336
+ (_resolve, reject) => {
337
+ options?.signal?.addEventListener(
338
+ 'abort',
339
+ () => reject(new Error('aborted')),
340
+ { once: true }
341
+ );
342
+ }
343
+ );
344
+ },
345
+ });
346
+
347
+ const caller = new AbortController();
348
+ const settled = execWithClientTimeout(
349
+ sandbox,
350
+ 'echo hi',
351
+ { signal: caller.signal },
352
+ 60000,
353
+ 'test'
354
+ ).catch((e) => e as Error);
355
+
356
+ await Promise.resolve();
357
+ expect(received).toBeDefined();
358
+ // The exec gets a composed signal, not the caller's directly.
359
+ expect(received).not.toBe(caller.signal);
360
+ expect(received?.aborted).toBe(false);
361
+
362
+ // A caller cancellation must reach the exec (not wait for the client timeout).
363
+ caller.abort();
364
+ await settled;
365
+ expect(received?.aborted).toBe(true);
366
+ });
367
+
368
+ it('strips a caller signal for native runtimes that cannot consume it', async () => {
369
+ let received: t.CloudflareSandboxExecOptions | undefined;
370
+ const sandbox = createRuntime({
371
+ // no supportsExecSignal -> native DO, which cannot clone/consume a signal
372
+ exec: async (_command, options) => {
373
+ received = options;
374
+ return { exitCode: 0, stdout: 'ok', stderr: '' };
375
+ },
376
+ });
377
+ const caller = new AbortController();
378
+
379
+ await execWithClientTimeout(
380
+ sandbox,
381
+ 'echo hi',
382
+ { cwd: '/workspace', signal: caller.signal },
383
+ 60000,
384
+ 'test'
385
+ );
386
+
387
+ expect(received).toBeDefined();
388
+ expect(received).not.toHaveProperty('signal');
389
+ });
390
+
260
391
  it('passes call-specific timeouts to the Cloudflare spawn wrapper', async () => {
261
392
  let execCommand = '';
262
393
  let execTimeout: number | undefined;
@@ -175,8 +175,8 @@ describe('local execution tools', () => {
175
175
  aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
176
176
  lang: 'py',
177
177
  code: [
178
- 'await write_file(file_path="ptc.txt", content="from local ptc")',
179
- 'contents = await read_file(file_path="ptc.txt")',
178
+ 'await write_file(path="ptc.txt", content="from local ptc")',
179
+ 'contents = await read_file(path="ptc.txt")',
180
180
  'print(contents)',
181
181
  ].join('\n'),
182
182
  }),
@@ -205,8 +205,8 @@ describe('local execution tools', () => {
205
205
  messages: [
206
206
  aiMessageWithToolCall(Constants.PROGRAMMATIC_TOOL_CALLING, {
207
207
  code: [
208
- 'write_file \'{"file_path":"bash-ptc.txt","content":"from bash ptc"}\'',
209
- 'read_file \'{"file_path":"bash-ptc.txt"}\'',
208
+ 'write_file \'{"path":"bash-ptc.txt","content":"from bash ptc"}\'',
209
+ 'read_file \'{"path":"bash-ptc.txt"}\'',
210
210
  ].join('\n'),
211
211
  }),
212
212
  ],
@@ -335,8 +335,8 @@ describe('LocalFileCheckpointer', () => {
335
335
 
336
336
  const writeTool = bundle.tools.find((tool_) => tool_.name === 'write_file');
337
337
  expect(writeTool).toBeDefined();
338
- await writeTool!.invoke({ file_path: 'cp.txt', content: 'first' });
339
- await writeTool!.invoke({ file_path: 'cp.txt', content: 'second' });
338
+ await writeTool!.invoke({ path: 'cp.txt', content: 'first' });
339
+ await writeTool!.invoke({ path: 'cp.txt', content: 'second' });
340
340
 
341
341
  const restored = await bundle.checkpointer!.rewind();
342
342
  expect(restored).toBe(1);
@@ -352,7 +352,7 @@ describe('local read tool guards', () => {
352
352
 
353
353
  const bundle = createLocalCodingToolBundle({ cwd });
354
354
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
355
- const result = await readTool!.invoke({ file_path: 'binary.bin' });
355
+ const result = await readTool!.invoke({ path: 'binary.bin' });
356
356
  expect(String(result)).toContain('binary file');
357
357
  });
358
358
 
@@ -366,7 +366,7 @@ describe('local read tool guards', () => {
366
366
  maxReadBytes: 1024,
367
367
  });
368
368
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
369
- const result = await readTool!.invoke({ file_path: 'big.txt' });
369
+ const result = await readTool!.invoke({ path: 'big.txt' });
370
370
  expect(String(result)).toContain('exceeds the 1024-byte read cap');
371
371
  });
372
372
 
@@ -380,7 +380,7 @@ describe('local read tool guards', () => {
380
380
  const bundle = createLocalCodingToolBundle({ cwd });
381
381
  const readTool = bundle.tools.find((t_) => t_.name === Constants.READ_FILE);
382
382
  await expect(
383
- readTool!.invoke({ file_path: 'escape/secret.txt' })
383
+ readTool!.invoke({ path: 'escape/secret.txt' })
384
384
  ).rejects.toThrow(/symlink escape/);
385
385
  });
386
386
  });
@@ -406,7 +406,7 @@ describe('local programmatic bridge auth', () => {
406
406
  code: [
407
407
  'import os, json, urllib.request, urllib.error',
408
408
  'url = os.environ["BRIDGE_PROBE_URL"] if "BRIDGE_PROBE_URL" in os.environ else __LIBRECHAT_TOOL_BRIDGE',
409
- 'body = json.dumps({"name":"read_file","input":{"file_path":"x"}}).encode("utf-8")',
409
+ 'body = json.dumps({"name":"read_file","input":{"path":"x"}}).encode("utf-8")',
410
410
  'try:',
411
411
  ' req = urllib.request.Request(url, data=body, headers={"Content-Type":"application/json"}, method="POST")',
412
412
  ' urllib.request.urlopen(req, timeout=5)',
@@ -438,7 +438,7 @@ describe('local edit fuzzy matching', () => {
438
438
  const bundle = createLocalCodingToolBundle({ cwd });
439
439
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
440
440
  const result = await editTool!.invoke({
441
- file_path: 'a.ts',
441
+ path: 'a.ts',
442
442
  // LLM emits a trailing-whitespace-stripped version.
443
443
  old_text:
444
444
  'function greet(name: string) {\n return `Hello, ${name}!`;\n}',
@@ -461,7 +461,7 @@ describe('local edit fuzzy matching', () => {
461
461
  const bundle = createLocalCodingToolBundle({ cwd });
462
462
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
463
463
  const result = await editTool!.invoke({
464
- file_path: 'a.ts',
464
+ path: 'a.ts',
465
465
  // LLM stripped the 4-space indent
466
466
  old_text: 'method() {\n return 1;\n}',
467
467
  new_text: 'method() {\n return 42;\n}',
@@ -480,7 +480,7 @@ describe('local edit fuzzy matching', () => {
480
480
  const bundle = createLocalCodingToolBundle({ cwd });
481
481
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
482
482
  const result = await editTool!.invoke({
483
- file_path: 'a.txt',
483
+ path: 'a.txt',
484
484
  old_text: 'second',
485
485
  new_text: 'SECOND',
486
486
  });
@@ -497,7 +497,7 @@ describe('local edit fuzzy matching', () => {
497
497
  const bundle = createLocalCodingToolBundle({ cwd });
498
498
  const editTool = bundle.tools.find((tt) => tt.name === 'edit_file');
499
499
  await editTool!.invoke({
500
- file_path: 'a.txt',
500
+ path: 'a.txt',
501
501
  old_text: 'two',
502
502
  new_text: 'TWO',
503
503
  });
@@ -512,7 +512,7 @@ describe('local edit fuzzy matching', () => {
512
512
  await fsWriteFile(file, BOM + 'hello\n', 'utf8');
513
513
  const bundle = createLocalCodingToolBundle({ cwd });
514
514
  const writeTool = bundle.tools.find((tt) => tt.name === 'write_file');
515
- await writeTool!.invoke({ file_path: 'a.txt', content: 'goodbye\n' });
515
+ await writeTool!.invoke({ path: 'a.txt', content: 'goodbye\n' });
516
516
  const raw = await fsReadFile(file, 'utf8');
517
517
  expect(raw.startsWith(BOM)).toBe(true);
518
518
  expect(raw.slice(1)).toBe('goodbye\n');
@@ -532,7 +532,7 @@ describe('local read attachments', () => {
532
532
  await fsWriteFile(file, tinyPng);
533
533
  const bundle = createLocalCodingToolBundle({ cwd });
534
534
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
535
- const result = await readTool!.invoke({ file_path: 'tiny.png' });
535
+ const result = await readTool!.invoke({ path: 'tiny.png' });
536
536
  expect(String(result)).toContain('binary file');
537
537
  });
538
538
 
@@ -552,7 +552,7 @@ describe('local read attachments', () => {
552
552
  const message = (await readTool!.invoke({
553
553
  id: 'call_image',
554
554
  name: Constants.READ_FILE,
555
- args: { file_path: 'tiny.png' },
555
+ args: { path: 'tiny.png' },
556
556
  type: 'tool_call',
557
557
  })) as { content: unknown; artifact: unknown };
558
558
  expect(Array.isArray(message.content)).toBe(true);
@@ -589,7 +589,7 @@ describe('local read attachments', () => {
589
589
  maxAttachmentBytes: 100,
590
590
  });
591
591
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
592
- const result = await readTool!.invoke({ file_path: 'big.png' });
592
+ const result = await readTool!.invoke({ path: 'big.png' });
593
593
  expect(String(result)).toMatch(/Refusing to embed/);
594
594
  });
595
595
 
@@ -602,7 +602,7 @@ describe('local read attachments', () => {
602
602
  attachReadAttachments: 'images-only',
603
603
  });
604
604
  const readTool = bundle.tools.find((tt) => tt.name === Constants.READ_FILE);
605
- const result = await readTool!.invoke({ file_path: 'a.txt' });
605
+ const result = await readTool!.invoke({ path: 'a.txt' });
606
606
  expect(String(result)).toContain('hello world');
607
607
  });
608
608
  });
@@ -662,7 +662,7 @@ describe('post-edit syntax check', () => {
662
662
  const message = (await writeTool!.invoke({
663
663
  id: 'call_w',
664
664
  name: 'write_file',
665
- args: { file_path: 'broken.js', content: 'function (\n' },
665
+ args: { path: 'broken.js', content: 'function (\n' },
666
666
  type: 'tool_call',
667
667
  })) as { content: string; artifact: { syntax_error?: string } };
668
668
  expect(message.content).toContain('[syntax-check warning');
@@ -680,7 +680,7 @@ describe('post-edit syntax check', () => {
680
680
  writeTool!.invoke({
681
681
  id: 'call_w',
682
682
  name: 'write_file',
683
- args: { file_path: 'broken.js', content: 'function (\n' },
683
+ args: { path: 'broken.js', content: 'function (\n' },
684
684
  type: 'tool_call',
685
685
  })
686
686
  ).rejects.toThrow(/syntax check failed/);
@@ -850,7 +850,7 @@ describe('codex review fixes', () => {
850
850
  const result = await readTool!.invoke({
851
851
  id: 'c',
852
852
  name: Constants.READ_FILE,
853
- args: { file_path: join(parent, 'shared/lib.ts') },
853
+ args: { path: join(parent, 'shared/lib.ts') },
854
854
  type: 'tool_call',
855
855
  });
856
856
  expect(JSON.stringify(result)).toContain('X');
@@ -2253,7 +2253,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2253
2253
  writeTool!.invoke({
2254
2254
  id: 'wf-strict',
2255
2255
  name: Constants.WRITE_FILE,
2256
- args: { file_path: file, content: 'function broken( {\n' },
2256
+ args: { path: file, content: 'function broken( {\n' },
2257
2257
  type: 'tool_call',
2258
2258
  })
2259
2259
  ).rejects.toThrow(/syntax check failed.*reverted/i);
@@ -2281,7 +2281,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2281
2281
  writeTool!.invoke({
2282
2282
  id: 'wf-strict-new',
2283
2283
  name: Constants.WRITE_FILE,
2284
- args: { file_path: file, content: 'function broken( {\n' },
2284
+ args: { path: file, content: 'function broken( {\n' },
2285
2285
  type: 'tool_call',
2286
2286
  })
2287
2287
  ).rejects.toThrow(/syntax check failed.*reverted/i);
@@ -2308,7 +2308,7 @@ describe('comprehensive review (round 14) — Codex P1 #37 + P2 #38/#40/#41', ()
2308
2308
  id: 'ef-strict',
2309
2309
  name: Constants.EDIT_FILE,
2310
2310
  args: {
2311
- file_path: file,
2311
+ path: file,
2312
2312
  old_text: 'return 1;',
2313
2313
  new_text: 'return broken(',
2314
2314
  },
@@ -1503,10 +1503,10 @@ for member in team:
1503
1503
  { registry, runId: 'r1' },
1504
1504
  'read_file',
1505
1505
  'call_1',
1506
- { file_path: '/tmp/x' }
1506
+ { path: '/tmp/x' }
1507
1507
  );
1508
1508
  expect(gate.denyReason).toBeUndefined();
1509
- expect(gate.input).toEqual({ file_path: '/tmp/x' });
1509
+ expect(gate.input).toEqual({ path: '/tmp/x' });
1510
1510
  });
1511
1511
 
1512
1512
  it('applies updatedInput to the inner tool args', async () => {
@@ -1520,7 +1520,7 @@ for member in team:
1520
1520
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
1521
1521
  async () => ({
1522
1522
  decision: 'allow',
1523
- updatedInput: { file_path: '/tmp/rewritten' },
1523
+ updatedInput: { path: '/tmp/rewritten' },
1524
1524
  }),
1525
1525
  ],
1526
1526
  });
@@ -1529,10 +1529,10 @@ for member in team:
1529
1529
  { registry, runId: 'r1' },
1530
1530
  'read_file',
1531
1531
  'call_1',
1532
- { file_path: '/tmp/original' }
1532
+ { path: '/tmp/original' }
1533
1533
  );
1534
1534
  expect(gate.denyReason).toBeUndefined();
1535
- expect(gate.input).toEqual({ file_path: '/tmp/rewritten' });
1535
+ expect(gate.input).toEqual({ path: '/tmp/rewritten' });
1536
1536
  });
1537
1537
 
1538
1538
  it('treats `ask` as fail-closed deny (HITL not reachable from bridge)', async () => {
@@ -9,9 +9,9 @@ import { Constants } from '@/common';
9
9
 
10
10
  describe('ReadFile', () => {
11
11
  describe('schema structure', () => {
12
- it('has file_path as required string property', () => {
13
- expect(ReadFileToolSchema.properties.file_path.type).toBe('string');
14
- expect(ReadFileToolSchema.required).toContain('file_path');
12
+ it('has path as required string property', () => {
13
+ expect(ReadFileToolSchema.properties.path.type).toBe('string');
14
+ expect(ReadFileToolSchema.required).toContain('path');
15
15
  });
16
16
 
17
17
  it('is an object type schema', () => {
@@ -1021,7 +1021,7 @@ describe('ToolNode code execution session management', () => {
1021
1021
  {
1022
1022
  id: 'call_rf',
1023
1023
  name: Constants.READ_FILE,
1024
- args: { file_path: '/mnt/data/data.csv' },
1024
+ args: { path: '/mnt/data/data.csv' },
1025
1025
  },
1026
1026
  ],
1027
1027
  });
@@ -1062,7 +1062,7 @@ describe('ToolNode code execution session management', () => {
1062
1062
  {
1063
1063
  id: 'call_rf2',
1064
1064
  name: Constants.READ_FILE,
1065
- args: { file_path: 'some-skill/notes.md' },
1065
+ args: { path: 'some-skill/notes.md' },
1066
1066
  },
1067
1067
  ],
1068
1068
  });
@@ -125,7 +125,7 @@ describe('workspace seam', () => {
125
125
  await writeTool.invoke({
126
126
  id: 'c1',
127
127
  name: 'write_file',
128
- args: { file_path: 'note.md', content: 'hi\n' },
128
+ args: { path: 'note.md', content: 'hi\n' },
129
129
  type: 'tool_call',
130
130
  });
131
131
  expect(tracked.writeFile).toHaveBeenCalled();
@@ -137,7 +137,7 @@ describe('workspace seam', () => {
137
137
  await readTool.invoke({
138
138
  id: 'c2',
139
139
  name: Constants.READ_FILE,
140
- args: { file_path: 'note.md' },
140
+ args: { path: 'note.md' },
141
141
  type: 'tool_call',
142
142
  });
143
143
  expect(tracked.stat).toHaveBeenCalled();
@@ -19,6 +19,8 @@ import {
19
19
  } from '@/tools/BashProgrammaticToolCalling';
20
20
  import { Constants } from '@/common';
21
21
  import {
22
+ clientExecTimeoutMs,
23
+ execWithClientTimeout,
22
24
  executeCloudflareCode,
23
25
  getCloudflareWorkspaceRoot,
24
26
  resolveCloudflareSandbox,
@@ -166,13 +168,16 @@ async function executeGeneratedCloudflareBash(
166
168
  const workspaceRoot = getCloudflareWorkspaceRoot(config);
167
169
  const shell = config.shell ?? 'bash';
168
170
  const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
169
- const result = await sandbox.exec(
171
+ const result = await execWithClientTimeout(
172
+ sandbox,
170
173
  withInSandboxTimeout(`${shell} -lc ${quoteShell(command)}`, timeoutMs),
171
174
  {
172
175
  cwd: workspaceRoot,
173
176
  env: config.env,
174
177
  timeout: outerTimeoutMs(timeoutMs),
175
- }
178
+ },
179
+ clientExecTimeoutMs(timeoutMs),
180
+ 'cloudflare bash programmatic exec'
176
181
  );
177
182
  const maxOutputChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
178
183
  return {
@@ -478,30 +483,30 @@ async def execute_code(lang, code, args=None):
478
483
  finally:
479
484
  shutil.rmtree(temp_dir, ignore_errors=True)
480
485
 
481
- async def read_file(file_path, offset=None, limit=None):
482
- resolved = _resolve(file_path)
486
+ async def read_file(path, offset=None, limit=None):
487
+ resolved = _resolve(path)
483
488
  with open(resolved, encoding="utf-8") as handle:
484
489
  return _line_window(handle.read(), offset, limit)
485
490
 
486
- async def write_file(file_path, content):
491
+ async def write_file(path, content):
487
492
  _assert_writable("write_file")
488
- resolved = _resolve(file_path)
493
+ resolved = _resolve(path)
489
494
  os.makedirs(os.path.dirname(resolved), exist_ok=True)
490
495
  existed = os.path.exists(resolved)
491
496
  with open(resolved, "w", encoding="utf-8") as handle:
492
497
  handle.write(content)
493
498
  return f"{'Overwrote' if existed else 'Created'} {resolved} ({len(content)} chars)."
494
499
 
495
- async def edit_file(file_path, old_text=None, new_text=None, edits=None):
500
+ async def edit_file(path, old_text=None, new_text=None, edits=None):
496
501
  _assert_writable("edit_file")
497
- resolved = _resolve(file_path)
502
+ resolved = _resolve(path)
498
503
  edits = edits or [{"old_text": old_text, "new_text": new_text}]
499
504
  content = open(resolved, encoding="utf-8").read()
500
505
  for edit in edits:
501
506
  old = edit.get("old_text") or ""
502
507
  new = edit.get("new_text") or ""
503
508
  if content.count(old) != 1:
504
- raise ValueError(f"Could not locate old_text exactly once in {file_path}")
509
+ raise ValueError(f"Could not locate old_text exactly once in {path}")
505
510
  content = content.replace(old, new, 1)
506
511
  open(resolved, "w", encoding="utf-8").write(content)
507
512
  return f"Applied {len(edits)} edit(s) to {resolved}."
@@ -881,13 +886,13 @@ async function execute_code(payload) {
881
886
  }
882
887
 
883
888
  async function read_file(payload) {
884
- const resolved = resolvePath(payload.file_path);
889
+ const resolved = resolvePath(payload.path);
885
890
  return lineWindow(await fsp.readFile(resolved, "utf8"), payload.offset, payload.limit);
886
891
  }
887
892
 
888
893
  async function write_file(payload) {
889
894
  assertWritable("write_file");
890
- const resolved = resolvePath(payload.file_path);
895
+ const resolved = resolvePath(payload.path);
891
896
  await fsp.mkdir(path.dirname(resolved), { recursive: true });
892
897
  const existed = fs.existsSync(resolved);
893
898
  await fsp.writeFile(resolved, payload.content, "utf8");
@@ -896,14 +901,14 @@ async function write_file(payload) {
896
901
 
897
902
  async function edit_file(payload) {
898
903
  assertWritable("edit_file");
899
- const resolved = resolvePath(payload.file_path);
904
+ const resolved = resolvePath(payload.path);
900
905
  const edits = payload.edits || [{ old_text: payload.old_text, new_text: payload.new_text }];
901
906
  let content = await fsp.readFile(resolved, "utf8");
902
907
  for (const edit of edits) {
903
908
  const oldText = edit.old_text || "";
904
909
  const newText = edit.new_text || "";
905
910
  if (oldText === "" || content.split(oldText).length - 1 !== 1) {
906
- throw new Error("Could not locate old_text exactly once in " + payload.file_path);
911
+ throw new Error("Could not locate old_text exactly once in " + payload.path);
907
912
  }
908
913
  content = content.replace(oldText, newText);
909
914
  }