@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,1162 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import type { DynamicStructuredTool } from '@langchain/core/tools';
3
+ import type * as t from '@/types';
4
+
5
+ /* eslint-disable no-useless-escape -- generated sandbox helper source needs escapes for emitted JS/Python string literals. */
6
+ import {
7
+ formatCompletedResponse,
8
+ normalizeToPythonIdentifier,
9
+ ProgrammaticToolCallingDescription,
10
+ ProgrammaticToolCallingName,
11
+ ProgrammaticToolCallingSchema,
12
+ filterToolsByUsage,
13
+ } from '@/tools/ProgrammaticToolCalling';
14
+ import {
15
+ BashProgrammaticToolCallingDescription,
16
+ BashProgrammaticToolCallingSchema,
17
+ filterBashToolsByUsage,
18
+ normalizeToBashIdentifier,
19
+ } from '@/tools/BashProgrammaticToolCalling';
20
+ import { Constants } from '@/common';
21
+ import {
22
+ executeCloudflareCode,
23
+ getCloudflareWorkspaceRoot,
24
+ resolveCloudflareSandbox,
25
+ validateCloudflareBashCommand,
26
+ } from './CloudflareSandboxExecutionEngine';
27
+
28
+ type ProgrammaticParams = {
29
+ code: string;
30
+ timeout?: number;
31
+ lang?: string;
32
+ runtime?: string;
33
+ language?: string;
34
+ };
35
+
36
+ const DEFAULT_TIMEOUT = 60000;
37
+ const MIN_TIMEOUT = 1000;
38
+ const MAX_TIMEOUT = 300000;
39
+ const DEFAULT_MAX_OUTPUT_CHARS = 200000;
40
+
41
+ type TimeoutSchema = {
42
+ type: 'integer';
43
+ minimum: number;
44
+ maximum: number;
45
+ default: number;
46
+ description: string;
47
+ };
48
+
49
+ type CloudflareProgrammaticToolCallingJsonSchema = {
50
+ type: 'object';
51
+ properties: typeof ProgrammaticToolCallingSchema.properties & {
52
+ timeout: TimeoutSchema;
53
+ lang: {
54
+ type: 'string';
55
+ enum: readonly ['py', 'python', 'bash', 'sh'];
56
+ default: 'bash';
57
+ description: string;
58
+ };
59
+ };
60
+ required: readonly ['code'];
61
+ };
62
+
63
+ type CloudflareBashProgrammaticToolCallingJsonSchema = {
64
+ type: 'object';
65
+ properties: typeof BashProgrammaticToolCallingSchema.properties & {
66
+ timeout: TimeoutSchema;
67
+ };
68
+ required: readonly ['code'];
69
+ };
70
+
71
+ const NATIVE_TOOL_NAMES = new Set<string>([
72
+ Constants.READ_FILE,
73
+ Constants.WRITE_FILE,
74
+ Constants.EDIT_FILE,
75
+ Constants.GREP_SEARCH,
76
+ Constants.GLOB_SEARCH,
77
+ Constants.LIST_DIRECTORY,
78
+ Constants.COMPILE_CHECK,
79
+ Constants.BASH_TOOL,
80
+ Constants.EXECUTE_CODE,
81
+ ]);
82
+
83
+ function normalizeTimeout(timeoutMs: number | undefined): number {
84
+ if (timeoutMs == null || !Number.isFinite(timeoutMs)) {
85
+ return DEFAULT_TIMEOUT;
86
+ }
87
+ return Math.max(MIN_TIMEOUT, Math.floor(timeoutMs));
88
+ }
89
+
90
+ function formatTimeout(timeoutMs: number): string {
91
+ return timeoutMs % 1000 === 0
92
+ ? `${timeoutMs / 1000} seconds`
93
+ : `${timeoutMs} milliseconds`;
94
+ }
95
+
96
+ function createTimeoutSchema(timeoutMs?: number): TimeoutSchema {
97
+ const defaultTimeout = normalizeTimeout(timeoutMs);
98
+ const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);
99
+ return {
100
+ type: 'integer',
101
+ minimum: MIN_TIMEOUT,
102
+ maximum: maxTimeout,
103
+ default: defaultTimeout,
104
+ description:
105
+ 'Maximum Cloudflare Sandbox execution time in milliseconds. ' +
106
+ `Default: ${formatTimeout(defaultTimeout)}. Max: ${formatTimeout(maxTimeout)}.`,
107
+ };
108
+ }
109
+
110
+ function clampExecutionTimeout(
111
+ requestedTimeoutMs: number | undefined,
112
+ configuredTimeoutMs: number | undefined
113
+ ): number {
114
+ const defaultTimeout = normalizeTimeout(configuredTimeoutMs);
115
+ const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);
116
+ if (requestedTimeoutMs == null || !Number.isFinite(requestedTimeoutMs)) {
117
+ return defaultTimeout;
118
+ }
119
+ return Math.min(
120
+ Math.max(MIN_TIMEOUT, Math.floor(requestedTimeoutMs)),
121
+ maxTimeout
122
+ );
123
+ }
124
+
125
+ function quoteShell(value: string): string {
126
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {
127
+ return value;
128
+ }
129
+ const escapedQuote = String.raw`'\''`;
130
+ return `'${value.replace(/'/g, escapedQuote)}'`;
131
+ }
132
+
133
+ function truncateOutput(
134
+ value: string,
135
+ maxChars = DEFAULT_MAX_OUTPUT_CHARS
136
+ ): string {
137
+ if (value.length <= maxChars) {
138
+ return value;
139
+ }
140
+ const head = Math.floor(maxChars * 0.6);
141
+ const tail = maxChars - head;
142
+ const omitted = value.length - maxChars;
143
+ return `${value.slice(0, head)}\n\n[... ${omitted} characters truncated ...]\n\n${value.slice(
144
+ value.length - tail
145
+ )}`;
146
+ }
147
+
148
+ function withInSandboxTimeout(command: string, timeoutMs: number): string {
149
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
150
+ return `timeout -k 2s ${timeoutSeconds}s ${command}`;
151
+ }
152
+
153
+ function outerTimeoutMs(timeoutMs: number): number {
154
+ return timeoutMs + 5000;
155
+ }
156
+
157
+ function isInSandboxTimeoutExit(exitCode: number | null): boolean {
158
+ return exitCode === 124 || exitCode === 137;
159
+ }
160
+
161
+ async function executeGeneratedCloudflareBash(
162
+ command: string,
163
+ config: t.CloudflareSandboxExecutionConfig
164
+ ): ReturnType<typeof executeCloudflareCode> {
165
+ const sandbox = await resolveCloudflareSandbox(config);
166
+ const workspaceRoot = getCloudflareWorkspaceRoot(config);
167
+ const shell = config.shell ?? 'bash';
168
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
169
+ const result = await sandbox.exec(
170
+ withInSandboxTimeout(`${shell} -lc ${quoteShell(command)}`, timeoutMs),
171
+ {
172
+ cwd: workspaceRoot,
173
+ env: config.env,
174
+ timeout: outerTimeoutMs(timeoutMs),
175
+ }
176
+ );
177
+ const maxOutputChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
178
+ return {
179
+ stdout: truncateOutput(result.stdout, maxOutputChars),
180
+ stderr: truncateOutput(result.stderr, maxOutputChars),
181
+ exitCode: result.exitCode,
182
+ timedOut: isInSandboxTimeoutExit(result.exitCode),
183
+ };
184
+ }
185
+
186
+ function createCloudflareProgrammaticToolCallingSchema(
187
+ config: t.CloudflareSandboxExecutionConfig
188
+ ): CloudflareProgrammaticToolCallingJsonSchema {
189
+ return {
190
+ ...ProgrammaticToolCallingSchema,
191
+ properties: {
192
+ ...ProgrammaticToolCallingSchema.properties,
193
+ timeout: createTimeoutSchema(config.timeoutMs),
194
+ lang: {
195
+ type: 'string',
196
+ enum: ['py', 'python', 'bash', 'sh'],
197
+ default: 'bash',
198
+ description:
199
+ 'Cloudflare Sandbox runtime for orchestration code. Defaults to bash; use py/python for Python orchestration.',
200
+ },
201
+ },
202
+ } as const;
203
+ }
204
+
205
+ function createCloudflareBashProgrammaticToolCallingSchema(
206
+ config: t.CloudflareSandboxExecutionConfig
207
+ ): CloudflareBashProgrammaticToolCallingJsonSchema {
208
+ return {
209
+ ...BashProgrammaticToolCallingSchema,
210
+ properties: {
211
+ ...BashProgrammaticToolCallingSchema.properties,
212
+ timeout: createTimeoutSchema(config.timeoutMs),
213
+ },
214
+ } as const;
215
+ }
216
+
217
+ function resolveRuntime(params: ProgrammaticParams): 'python' | 'bash' {
218
+ const raw = params.lang ?? params.runtime ?? params.language ?? 'bash';
219
+ return raw === 'py' || raw === 'python' ? 'python' : 'bash';
220
+ }
221
+
222
+ function filterNativeTools(
223
+ toolDefs: t.LCTool[],
224
+ code: string,
225
+ runtime: 'python' | 'bash'
226
+ ): t.LCTool[] {
227
+ const nativeDefs = toolDefs.filter((def) => NATIVE_TOOL_NAMES.has(def.name));
228
+ const filter =
229
+ runtime === 'bash' ? filterBashToolsByUsage : filterToolsByUsage;
230
+ return filter(nativeDefs, code);
231
+ }
232
+
233
+ function indent(code: string, spaces = 4): string {
234
+ const prefix = ' '.repeat(spaces);
235
+ return code
236
+ .split('\n')
237
+ .map((line) => (line === '' ? line : prefix + line))
238
+ .join('\n');
239
+ }
240
+
241
+ function pythonBoolean(value: boolean | undefined): 'True' | 'False' {
242
+ return value === true ? 'True' : 'False';
243
+ }
244
+
245
+ function createPythonNativeToolSource(
246
+ config: t.CloudflareSandboxExecutionConfig,
247
+ workspaceRoot: string
248
+ ): string {
249
+ return `
250
+ import asyncio, fnmatch, glob, json, os, pathlib, re, shlex, shutil, subprocess, sys, tempfile
251
+
252
+ WORKSPACE = ${JSON.stringify(workspaceRoot)}
253
+ SHELL = ${JSON.stringify(config.shell ?? 'bash')}
254
+ READ_ONLY = ${pythonBoolean(config.readOnly)}
255
+ ALLOW_DANGEROUS_COMMANDS = ${pythonBoolean(config.allowDangerousCommands)}
256
+ DESTRUCTIVE_TARGET = r"(?:/|~|\\$\\{?HOME\\}?|\\.)(?:/?\\.?\\*|/)?"
257
+ DANGEROUS_COMMAND_PATTERNS = [
258
+ re.compile(r"\\brm\\s+(?:-[^\\s]*[rf][^\\s]*\\s+|-[^\\s]*[r][^\\s]*\\s+-[^\\s]*[f][^\\s]*\\s+)(?:--\\s+)?" + DESTRUCTIVE_TARGET + r"\\s*(?:$|[;&|])"),
259
+ re.compile(r"\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\b"),
260
+ re.compile(r"\\bdd\\s+[^;&|]*\\bof=/dev/"),
261
+ re.compile(r"\\bchmod\\s+-R\\s+(?:777|a\\+w)\\s+(?:--\\s+)?" + DESTRUCTIVE_TARGET + r"(?:$|\\s|[;&|])"),
262
+ re.compile(r"\\bchown\\s+-R\\s+[^;&|]+\\s+(?:--\\s+)?" + DESTRUCTIVE_TARGET + r"(?:$|\\s|[;&|])"),
263
+ re.compile(r":\\s*\\(\\s*\\)\\s*\\{\\s*:\\s*\\|\\s*:\\s*&\\s*\\}\\s*;\\s*:"),
264
+ ]
265
+ QUOTED_DESTRUCTIVE_PATTERNS = [
266
+ re.compile(r"\\brm\\s+(?:-[^\\s]*[rf][^\\s]*\\s+){1,3}(?:--\\s+)?[\\"']" + DESTRUCTIVE_TARGET + r"[\\"']"),
267
+ re.compile(r"\\bchmod\\s+-R\\s+(?:777|a\\+w)\\s+(?:--\\s+)?[\\"']" + DESTRUCTIVE_TARGET + r"[\\"']"),
268
+ re.compile(r"\\bchown\\s+-R\\s+[^;&|]+\\s+(?:--\\s+)?[\\"']" + DESTRUCTIVE_TARGET + r"[\\"']"),
269
+ ]
270
+ NESTED_SHELL_PREFIX = r"(?:(?:ba|z|da|k)?sh|eval)\\s+(?:-l?c\\s+)?"
271
+ NESTED_SHELL_DESTRUCTIVE_PATTERNS = [
272
+ re.compile(NESTED_SHELL_PREFIX + r"[\\"'][^\\"']*\\brm\\s+-[^\\s\\"']*[rf][^\\s\\"']*\\s+(?:--\\s+)?(?:/|~|\\$\\{?HOME\\}?|\\.)"),
273
+ re.compile(NESTED_SHELL_PREFIX + r"[\\"'][^\\"']*\\bchmod\\s+-R\\s+(?:777|a\\+w)\\s+(?:--\\s+)?(?:/|~|\\$\\{?HOME\\}?|\\.)"),
274
+ re.compile(NESTED_SHELL_PREFIX + r"[\\"'][^\\"']*\\bchown\\s+-R\\s+[^;&|]+\\s+(?:--\\s+)?(?:/|~|\\$\\{?HOME\\}?|\\.)"),
275
+ ]
276
+ MUTATING_COMMAND_PATTERN = re.compile(r"\\b(?:rm|mv|cp|touch|mkdir|rmdir|ln|truncate|tee|sed\\s+-i|perl\\s+-pi|python(?:3)?\\s+-c|node\\s+-e|npm\\s+(?:install|ci|update|publish)|pnpm\\s+(?:install|update|publish)|yarn\\s+(?:install|add|publish)|git\\s+(?:add|commit|checkout|switch|reset|clean|rebase|merge|push|pull|stash|tag|branch)|chmod|chown)\\b|(?:^|[^<])>\\s*[^&]|\\bcat\\s+[^|;&]*>\\s*")
277
+ PROTECTED_TARGET_ARG_RE = re.compile(r"^(?:/|~|\\$\\{?HOME\\}?|\\.)(?:/?\\.?\\*|/)?$")
278
+ DESTRUCTIVE_OP_IN_COMMAND_RE = re.compile(r"\\b(?:rm\\s+-[^\\s]*[rf]|chmod\\s+-R|chown\\s+-R)\\b")
279
+
280
+ def _is_within_workspace(file_path):
281
+ resolved = os.path.abspath(file_path)
282
+ root = os.path.abspath(WORKSPACE)
283
+ return os.path.commonpath([root, resolved]) == root
284
+
285
+ def _resolve(file_path="."):
286
+ raw = file_path or "."
287
+ candidate = raw if os.path.isabs(raw) else os.path.join(WORKSPACE, raw)
288
+ resolved = os.path.abspath(candidate)
289
+ if not _is_within_workspace(resolved):
290
+ raise ValueError(f"Path is outside the Cloudflare sandbox workspace: {file_path}")
291
+ return resolved
292
+
293
+ def _assert_writable(tool_name):
294
+ if READ_ONLY:
295
+ raise PermissionError(f"{tool_name} is blocked in read-only Cloudflare sandbox mode.")
296
+
297
+ def _strip_quoted_content(command):
298
+ output = []
299
+ quote = None
300
+ escaped = False
301
+ index = 0
302
+ while index < len(command):
303
+ char = command[index]
304
+ if escaped:
305
+ escaped = False
306
+ output.append(" ")
307
+ index += 1
308
+ continue
309
+ if char == "\\\\":
310
+ escaped = True
311
+ output.append(" ")
312
+ index += 1
313
+ continue
314
+ if quote is not None:
315
+ if char == quote:
316
+ quote = None
317
+ output.append(" ")
318
+ index += 1
319
+ continue
320
+ if char in ("'", '"', "\`"):
321
+ quote = char
322
+ output.append(" ")
323
+ index += 1
324
+ continue
325
+ if char == "#":
326
+ while index < len(command) and command[index] != "\\n":
327
+ output.append(" ")
328
+ index += 1
329
+ output.append("\\n")
330
+ index += 1
331
+ continue
332
+ output.append(char)
333
+ index += 1
334
+ return "".join(output)
335
+
336
+ def _validate_bash_command(command, args=None):
337
+ errors = []
338
+ normalized = _strip_quoted_content(command)
339
+ if command.strip() == "":
340
+ errors.append("Command is empty.")
341
+ if "\\0" in command:
342
+ errors.append("Command contains a NUL byte.")
343
+ if not ALLOW_DANGEROUS_COMMANDS:
344
+ if any(pattern.search(normalized) for pattern in DANGEROUS_COMMAND_PATTERNS):
345
+ errors.append("Command matches a destructive command pattern.")
346
+ elif any(pattern.search(command) for pattern in QUOTED_DESTRUCTIVE_PATTERNS):
347
+ errors.append("Command matches a destructive command pattern (quoted target).")
348
+ elif any(pattern.search(command) for pattern in NESTED_SHELL_DESTRUCTIVE_PATTERNS):
349
+ errors.append("Command matches a destructive command pattern (nested shell payload).")
350
+ elif args and DESTRUCTIVE_OP_IN_COMMAND_RE.search(command):
351
+ offending = next((str(arg) for arg in args if PROTECTED_TARGET_ARG_RE.search(str(arg))), None)
352
+ if offending is not None:
353
+ errors.append(f"Command matches a destructive command pattern (protected target \\"{offending}\\" passed via positional arg).")
354
+ if READ_ONLY and MUTATING_COMMAND_PATTERN.search(normalized):
355
+ errors.append("Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.")
356
+ if errors:
357
+ raise ValueError("\\n".join(errors))
358
+
359
+ def _line_window(content, offset=None, limit=None):
360
+ start = max((offset or 1) - 1, 0)
361
+ lines = content.split("\\n")
362
+ selected = lines[start:] if not limit or limit <= 0 else lines[start:start + limit]
363
+ return "\\n".join(f"{start + idx + 1:6d}\\t{line}" for idx, line in enumerate(selected))
364
+
365
+ def _run(command, timeout=None, args=None):
366
+ _validate_bash_command(command, args=args)
367
+ completed = subprocess.run(
368
+ [SHELL, "-lc", command, "--"] + [str(arg) for arg in (args or [])],
369
+ cwd=WORKSPACE,
370
+ capture_output=True,
371
+ text=True,
372
+ timeout=(timeout / 1000 if timeout else None),
373
+ )
374
+ return {
375
+ "stdout": completed.stdout,
376
+ "stderr": completed.stderr,
377
+ "exit_code": completed.returncode,
378
+ }
379
+
380
+ def _format_run(result):
381
+ text = ""
382
+ if result.get("stdout"):
383
+ text += f"stdout:\\n{result['stdout']}\\n"
384
+ else:
385
+ text += "stdout: Empty. Ensure you're writing output explicitly.\\n"
386
+ if result.get("stderr"):
387
+ text += f"stderr:\\n{result['stderr']}\\n"
388
+ if result.get("exit_code") not in (None, 0):
389
+ text += f"exit_code: {result['exit_code']}\\n"
390
+ text += f"working_directory: {WORKSPACE}"
391
+ return text.strip()
392
+
393
+ def _detect_compile_command():
394
+ if os.path.exists(os.path.join(WORKSPACE, "tsconfig.json")):
395
+ return "typescript", "npx --no-install tsc --noEmit", "tsconfig.json present"
396
+ package_json = os.path.join(WORKSPACE, "package.json")
397
+ if os.path.exists(package_json):
398
+ try:
399
+ if '"typescript"' in open(package_json, encoding="utf-8").read():
400
+ return "typescript", "npx --no-install tsc --noEmit", "package.json declares typescript"
401
+ except Exception:
402
+ pass
403
+ if os.path.exists(os.path.join(WORKSPACE, "Cargo.toml")):
404
+ return "rust", "cargo check --message-format=short", "Cargo.toml present"
405
+ if os.path.exists(os.path.join(WORKSPACE, "go.mod")):
406
+ return "go", "go vet ./...", "go.mod present"
407
+ if any(os.path.exists(os.path.join(WORKSPACE, name)) for name in ["pyproject.toml", "setup.py", "setup.cfg"]):
408
+ return "python-compile", "python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')", "Python project"
409
+ return "unknown", "", "no recognised project marker"
410
+
411
+ async def bash_tool(command, args=None):
412
+ return _format_run(_run(command, args=args))
413
+
414
+ async def execute_code(lang, code, args=None):
415
+ args = args or []
416
+ temp_dir = tempfile.mkdtemp(prefix="lc-ptc-", dir=WORKSPACE)
417
+ try:
418
+ def q(value):
419
+ import shlex
420
+ return shlex.quote(str(value))
421
+ arg_text = " ".join(q(arg) for arg in args)
422
+ if lang in ("py", "python"):
423
+ file_path = os.path.join(temp_dir, "main.py")
424
+ open(file_path, "w", encoding="utf-8").write(code)
425
+ return _format_run(_run(f"python3 {q(file_path)} {arg_text}"))
426
+ if lang in ("js", "javascript"):
427
+ file_path = os.path.join(temp_dir, "main.js")
428
+ open(file_path, "w", encoding="utf-8").write(code)
429
+ return _format_run(_run(f"node {q(file_path)} {arg_text}"))
430
+ if lang in ("ts", "typescript"):
431
+ file_path = os.path.join(temp_dir, "main.ts")
432
+ open(file_path, "w", encoding="utf-8").write(code)
433
+ return _format_run(_run(f"npx --no-install tsx {q(file_path)} {arg_text}"))
434
+ if lang == "php":
435
+ file_path = os.path.join(temp_dir, "main.php")
436
+ open(file_path, "w", encoding="utf-8").write(code)
437
+ return _format_run(_run(f"php {q(file_path)} {arg_text}"))
438
+ if lang == "go":
439
+ file_path = os.path.join(temp_dir, "main.go")
440
+ open(file_path, "w", encoding="utf-8").write(code)
441
+ return _format_run(_run(f"go run {q(file_path)} {arg_text}"))
442
+ if lang == "rs":
443
+ file_path = os.path.join(temp_dir, "main.rs")
444
+ open(file_path, "w", encoding="utf-8").write(code)
445
+ binary = os.path.join(temp_dir, "main-rs")
446
+ return _format_run(_run(f"rustc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}"))
447
+ if lang == "c":
448
+ file_path = os.path.join(temp_dir, "main.c")
449
+ open(file_path, "w", encoding="utf-8").write(code)
450
+ binary = os.path.join(temp_dir, "main-c")
451
+ return _format_run(_run(f"cc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}"))
452
+ if lang == "cpp":
453
+ file_path = os.path.join(temp_dir, "main.cpp")
454
+ open(file_path, "w", encoding="utf-8").write(code)
455
+ binary = os.path.join(temp_dir, "main-cpp")
456
+ return _format_run(_run(f"c++ {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}"))
457
+ if lang == "java":
458
+ file_path = os.path.join(temp_dir, "Main.java")
459
+ open(file_path, "w", encoding="utf-8").write(code)
460
+ return _format_run(_run(f"javac {q(file_path)} && java -cp {q(temp_dir)} Main {arg_text}"))
461
+ if lang == "r":
462
+ file_path = os.path.join(temp_dir, "main.R")
463
+ open(file_path, "w", encoding="utf-8").write(code)
464
+ return _format_run(_run(f"Rscript {q(file_path)} {arg_text}"))
465
+ if lang == "d":
466
+ file_path = os.path.join(temp_dir, "main.d")
467
+ open(file_path, "w", encoding="utf-8").write(code)
468
+ binary = os.path.join(temp_dir, "main-d")
469
+ return _format_run(_run(f"dmd {q(file_path)} -of={q(binary)} && {q(binary)} {arg_text}"))
470
+ if lang == "f90":
471
+ file_path = os.path.join(temp_dir, "main.f90")
472
+ open(file_path, "w", encoding="utf-8").write(code)
473
+ binary = os.path.join(temp_dir, "main-f90")
474
+ return _format_run(_run(f"gfortran {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}"))
475
+ if lang in ("bash", "sh"):
476
+ return _format_run(_run(code, args=args))
477
+ raise ValueError(f"Unsupported Cloudflare sandbox runtime: {lang}")
478
+ finally:
479
+ shutil.rmtree(temp_dir, ignore_errors=True)
480
+
481
+ async def read_file(file_path, offset=None, limit=None):
482
+ resolved = _resolve(file_path)
483
+ with open(resolved, encoding="utf-8") as handle:
484
+ return _line_window(handle.read(), offset, limit)
485
+
486
+ async def write_file(file_path, content):
487
+ _assert_writable("write_file")
488
+ resolved = _resolve(file_path)
489
+ os.makedirs(os.path.dirname(resolved), exist_ok=True)
490
+ existed = os.path.exists(resolved)
491
+ with open(resolved, "w", encoding="utf-8") as handle:
492
+ handle.write(content)
493
+ return f"{'Overwrote' if existed else 'Created'} {resolved} ({len(content)} chars)."
494
+
495
+ async def edit_file(file_path, old_text=None, new_text=None, edits=None):
496
+ _assert_writable("edit_file")
497
+ resolved = _resolve(file_path)
498
+ edits = edits or [{"old_text": old_text, "new_text": new_text}]
499
+ content = open(resolved, encoding="utf-8").read()
500
+ for edit in edits:
501
+ old = edit.get("old_text") or ""
502
+ new = edit.get("new_text") or ""
503
+ if content.count(old) != 1:
504
+ raise ValueError(f"Could not locate old_text exactly once in {file_path}")
505
+ content = content.replace(old, new, 1)
506
+ open(resolved, "w", encoding="utf-8").write(content)
507
+ return f"Applied {len(edits)} edit(s) to {resolved}."
508
+
509
+ async def list_directory(path="."):
510
+ resolved = _resolve(path)
511
+ entries = []
512
+ for name in sorted(os.listdir(resolved)):
513
+ full = os.path.join(resolved, name)
514
+ entries.append(("dir " if os.path.isdir(full) else "file") + "\\t" + name)
515
+ return "\\n".join(entries) or "Directory is empty."
516
+
517
+ async def grep_search(pattern, path=".", glob=None, max_results=200):
518
+ root = _resolve(path)
519
+ regex = re.compile(pattern)
520
+ out = []
521
+ for current, dirs, files in os.walk(root):
522
+ dirs[:] = [d for d in dirs if d not in {".git", "node_modules", ".venv", "dist", "build"}]
523
+ for name in files:
524
+ rel = os.path.relpath(os.path.join(current, name), root)
525
+ if glob and not fnmatch.fnmatch(rel, glob):
526
+ continue
527
+ try:
528
+ for line_no, line in enumerate(open(os.path.join(current, name), encoding="utf-8", errors="ignore"), 1):
529
+ if regex.search(line):
530
+ out.append(f"{os.path.join(current, name)}:{line_no}:{line.rstrip()}")
531
+ if len(out) >= max_results:
532
+ return "\\n".join(out)
533
+ except Exception:
534
+ pass
535
+ return "\\n".join(out) if out else "No matches found."
536
+
537
+ async def glob_search(pattern, path=".", max_results=200):
538
+ root = _resolve(path)
539
+ target = pattern if os.path.isabs(pattern) else os.path.join(root, pattern)
540
+ matches = []
541
+ for match in glob_module.glob(target, recursive=True):
542
+ resolved = os.path.abspath(match)
543
+ if _is_within_workspace(resolved):
544
+ matches.append(resolved)
545
+ if len(matches) >= max_results:
546
+ break
547
+ return "\\n".join(matches) if matches else "No files found."
548
+
549
+ async def compile_check(command=None, timeout_ms=None):
550
+ kind, detected, reason = _detect_compile_command()
551
+ command = command or detected
552
+ if not command:
553
+ return f"compile_check: {reason}. Pass an explicit command to override."
554
+ result = _run(command, timeout_ms)
555
+ status = "PASSED" if result["exit_code"] == 0 else "FAILED"
556
+ return f"compile_check ({kind}) {status} via {command}\\n\\nstdout:\\n{result['stdout']}\\nstderr:\\n{result['stderr']}\\nworking_directory: {WORKSPACE}\\nreason: {reason}"
557
+
558
+ # Avoid shadowing the glob_search function argument named "glob".
559
+ glob_module = glob
560
+ `.trim();
561
+ }
562
+
563
+ function createNodeNativeToolSource(
564
+ config: t.CloudflareSandboxExecutionConfig,
565
+ workspaceRoot: string
566
+ ): string {
567
+ return `
568
+ const fs = require("fs");
569
+ const fsp = fs.promises;
570
+ const path = require("path");
571
+ const cp = require("child_process");
572
+
573
+ const WORKSPACE = ${JSON.stringify(workspaceRoot)};
574
+ const SHELL = ${JSON.stringify(config.shell ?? 'bash')};
575
+ const READ_ONLY = ${JSON.stringify(config.readOnly === true)};
576
+ const ALLOW_DANGEROUS_COMMANDS = ${JSON.stringify(config.allowDangerousCommands === true)};
577
+ const DESTRUCTIVE_TARGET = "(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:\\\\/?\\\\.?\\\\*|\\\\/)?";
578
+ const DANGEROUS_COMMAND_PATTERNS = [
579
+ new RegExp("\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+|-[^\\\\s]*[r][^\\\\s]*\\\\s+-[^\\\\s]*[f][^\\\\s]*\\\\s+)(?:--\\\\s+)?" + DESTRUCTIVE_TARGET + "\\\\s*(?:$|[;&|])"),
580
+ /\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\b/,
581
+ /\\bdd\\s+[^;&|]*\\bof=\\/dev\\//,
582
+ new RegExp("\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?" + DESTRUCTIVE_TARGET + "(?:$|\\\\s|[;&|])"),
583
+ new RegExp("\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?" + DESTRUCTIVE_TARGET + "(?:$|\\\\s|[;&|])"),
584
+ /:\\s*\\(\\s*\\)\\s*\\{\\s*:\\s*\\|\\s*:\\s*&\\s*\\}\\s*;\\s*:/,
585
+ ];
586
+ const QUOTED_DESTRUCTIVE_PATTERNS = [
587
+ new RegExp("\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+){1,3}(?:--\\\\s+)?[\\\"']" + DESTRUCTIVE_TARGET + "[\\\"']"),
588
+ new RegExp("\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?[\\\"']" + DESTRUCTIVE_TARGET + "[\\\"']"),
589
+ new RegExp("\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?[\\\"']" + DESTRUCTIVE_TARGET + "[\\\"']"),
590
+ ];
591
+ const NESTED_SHELL_PREFIX = "(?:(?:ba|z|da|k)?sh|eval)\\\\s+(?:-l?c\\\\s+)?";
592
+ const NESTED_SHELL_DESTRUCTIVE_PATTERNS = [
593
+ new RegExp(NESTED_SHELL_PREFIX + "[\\\"'][^\\\"']*\\\\brm\\\\s+-[^\\\\s\\\"']*[rf][^\\\\s\\\"']*\\\\s+(?:--\\\\s+)?(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)"),
594
+ new RegExp(NESTED_SHELL_PREFIX + "[\\\"'][^\\\"']*\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)"),
595
+ new RegExp(NESTED_SHELL_PREFIX + "[\\\"'][^\\\"']*\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)"),
596
+ ];
597
+ const MUTATING_COMMAND_PATTERN = /\\b(?:rm|mv|cp|touch|mkdir|rmdir|ln|truncate|tee|sed\\s+-i|perl\\s+-pi|python(?:3)?\\s+-c|node\\s+-e|npm\\s+(?:install|ci|update|publish)|pnpm\\s+(?:install|update|publish)|yarn\\s+(?:install|add|publish)|git\\s+(?:add|commit|checkout|switch|reset|clean|rebase|merge|push|pull|stash|tag|branch)|chmod|chown)\\b|(?:^|[^<])>\\s*[^&]|\\bcat\\s+[^|;&]*>\\s*/;
598
+ const PROTECTED_TARGET_ARG_RE = /^(?:\\/|~|\\$\\{?HOME\\}?|\\.)(?:\\/?\\.?\\*|\\/)?$/;
599
+ const DESTRUCTIVE_OP_IN_COMMAND_RE = /\\b(?:rm\\s+-[^\\s]*[rf]|chmod\\s+-R|chown\\s+-R)\\b/;
600
+
601
+ function resolvePath(filePath) {
602
+ const raw = filePath || ".";
603
+ const candidate = path.isAbsolute(raw) ? raw : path.join(WORKSPACE, raw);
604
+ const resolved = path.resolve(candidate);
605
+ const root = path.resolve(WORKSPACE);
606
+ const relative = path.relative(root, resolved);
607
+ if (relative && (relative.startsWith("..") || path.isAbsolute(relative))) {
608
+ throw new Error("Path is outside the Cloudflare sandbox workspace: " + filePath);
609
+ }
610
+ return resolved;
611
+ }
612
+
613
+ function assertWritable(toolName) {
614
+ if (READ_ONLY) {
615
+ throw new Error(toolName + " is blocked in read-only Cloudflare sandbox mode.");
616
+ }
617
+ }
618
+
619
+ function stripQuotedContent(command) {
620
+ let output = "";
621
+ let quote;
622
+ let escaped = false;
623
+ for (let index = 0; index < command.length; index++) {
624
+ const char = command[index];
625
+ if (escaped) {
626
+ escaped = false;
627
+ output += " ";
628
+ continue;
629
+ }
630
+ if (char === "\\\\") {
631
+ escaped = true;
632
+ output += " ";
633
+ continue;
634
+ }
635
+ if (quote != null) {
636
+ if (char === quote) quote = undefined;
637
+ output += " ";
638
+ continue;
639
+ }
640
+ if (char === "\\\"" || char === "'" || char === "\`") {
641
+ quote = char;
642
+ output += " ";
643
+ continue;
644
+ }
645
+ if (char === "#") {
646
+ while (index < command.length && command[index] !== "\\n") {
647
+ output += " ";
648
+ index += 1;
649
+ }
650
+ output += "\\n";
651
+ continue;
652
+ }
653
+ output += char;
654
+ }
655
+ return output;
656
+ }
657
+
658
+ function validateBashCommand(command, args) {
659
+ const errors = [];
660
+ const normalized = stripQuotedContent(command);
661
+ if (command.trim() === "") {
662
+ errors.push("Command is empty.");
663
+ }
664
+ if (command.includes("\\0")) {
665
+ errors.push("Command contains a NUL byte.");
666
+ }
667
+ if (!ALLOW_DANGEROUS_COMMANDS) {
668
+ if (DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
669
+ errors.push("Command matches a destructive command pattern.");
670
+ } else if (QUOTED_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
671
+ errors.push("Command matches a destructive command pattern (quoted target).");
672
+ } else if (NESTED_SHELL_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {
673
+ errors.push("Command matches a destructive command pattern (nested shell payload).");
674
+ } else if ((args || []).length > 0 && DESTRUCTIVE_OP_IN_COMMAND_RE.test(command)) {
675
+ const offending = (args || []).map(String).find((arg) => PROTECTED_TARGET_ARG_RE.test(arg));
676
+ if (offending !== undefined) {
677
+ errors.push("Command matches a destructive command pattern (protected target \\"" + offending + "\\" passed via positional arg).");
678
+ }
679
+ }
680
+ }
681
+ if (READ_ONLY && MUTATING_COMMAND_PATTERN.test(normalized)) {
682
+ errors.push("Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.");
683
+ }
684
+ if (errors.length > 0) {
685
+ throw new Error(errors.join("\\n"));
686
+ }
687
+ }
688
+
689
+ function lineWindow(content, offset, limit) {
690
+ const start = Math.max((offset || 1) - 1, 0);
691
+ const lines = content.split("\\n");
692
+ const selected = !limit || limit <= 0 ? lines.slice(start) : lines.slice(start, start + limit);
693
+ return selected.map((line, index) => String(start + index + 1).padStart(6, " ") + "\\t" + line).join("\\n");
694
+ }
695
+
696
+ function quote(value) {
697
+ const text = String(value);
698
+ if (text === "") return "''";
699
+ if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(text)) return text;
700
+ return "'" + text.replace(/'/g, "'\\\\''") + "'";
701
+ }
702
+
703
+ function run(command, timeoutMs, args) {
704
+ validateBashCommand(command, args);
705
+ return new Promise((resolve) => {
706
+ const child = cp.spawn(SHELL, ["-lc", command, "--", ...((args || []).map(String))], {
707
+ cwd: WORKSPACE,
708
+ env: process.env,
709
+ });
710
+ let stdout = "";
711
+ let stderr = "";
712
+ let timedOut = false;
713
+ const timer = timeoutMs
714
+ ? setTimeout(() => {
715
+ timedOut = true;
716
+ child.kill("SIGTERM");
717
+ }, timeoutMs)
718
+ : null;
719
+
720
+ child.stdout.on("data", (chunk) => {
721
+ stdout += chunk.toString();
722
+ });
723
+ child.stderr.on("data", (chunk) => {
724
+ stderr += chunk.toString();
725
+ });
726
+ child.on("error", (error) => {
727
+ if (timer) clearTimeout(timer);
728
+ resolve({ stdout, stderr: stderr + error.message, exit_code: 1, timed_out: timedOut });
729
+ });
730
+ child.on("close", (code) => {
731
+ if (timer) clearTimeout(timer);
732
+ resolve({ stdout, stderr, exit_code: timedOut ? null : code, timed_out: timedOut });
733
+ });
734
+ });
735
+ }
736
+
737
+ function formatRun(result) {
738
+ let text = "";
739
+ if (result.stdout) {
740
+ text += "stdout:\\n" + result.stdout + "\\n";
741
+ } else {
742
+ text += "stdout: Empty. Ensure you're writing output explicitly.\\n";
743
+ }
744
+ if (result.stderr) {
745
+ text += "stderr:\\n" + result.stderr + "\\n";
746
+ }
747
+ if (result.timed_out) {
748
+ text += "timed_out: true\\n";
749
+ }
750
+ if (result.exit_code !== null && result.exit_code !== undefined && result.exit_code !== 0) {
751
+ text += "exit_code: " + result.exit_code + "\\n";
752
+ }
753
+ text += "working_directory: " + WORKSPACE;
754
+ return text.trim();
755
+ }
756
+
757
+ async function detectCompileCommand() {
758
+ async function exists(name) {
759
+ try {
760
+ await fsp.access(path.join(WORKSPACE, name));
761
+ return true;
762
+ } catch {
763
+ return false;
764
+ }
765
+ }
766
+ if (await exists("tsconfig.json")) {
767
+ return ["typescript", "npx --no-install tsc --noEmit", "tsconfig.json present"];
768
+ }
769
+ if (await exists("package.json")) {
770
+ try {
771
+ if ((await fsp.readFile(path.join(WORKSPACE, "package.json"), "utf8")).includes('"typescript"')) {
772
+ return ["typescript", "npx --no-install tsc --noEmit", "package.json declares typescript"];
773
+ }
774
+ } catch {}
775
+ }
776
+ if (await exists("Cargo.toml")) return ["rust", "cargo check --message-format=short", "Cargo.toml present"];
777
+ if (await exists("go.mod")) return ["go", "go vet ./...", "go.mod present"];
778
+ if (await exists("pyproject.toml") || await exists("setup.py") || await exists("setup.cfg")) {
779
+ return ["python-compile", "python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')", "Python project"];
780
+ }
781
+ return ["unknown", "", "no recognised project marker"];
782
+ }
783
+
784
+ function globToRegExp(pattern) {
785
+ const escaped = pattern.replace(/[|\\\\{}()[\\]^$+*?.]/g, "\\\\$&");
786
+ return new RegExp("^" + escaped.replace(/\\\\\\*\\\\\\*/g, ".*").replace(/\\\\\\*/g, "[^/]*") + "$");
787
+ }
788
+
789
+ function globMatch(relativePath, pattern) {
790
+ const matcher = globToRegExp(pattern);
791
+ return matcher.test(relativePath) || matcher.test(path.basename(relativePath));
792
+ }
793
+
794
+ async function walkFiles(root, visit) {
795
+ const entries = await fsp.readdir(root, { withFileTypes: true });
796
+ for (const entry of entries) {
797
+ const full = path.join(root, entry.name);
798
+ if (entry.isDirectory()) {
799
+ if ([".git", "node_modules", ".venv", "dist", "build"].includes(entry.name)) continue;
800
+ await walkFiles(full, visit);
801
+ } else if (entry.isFile()) {
802
+ await visit(full);
803
+ }
804
+ }
805
+ }
806
+
807
+ async function bash_tool(payload) {
808
+ return formatRun(await run(payload.command, undefined, payload.args));
809
+ }
810
+
811
+ async function execute_code(payload) {
812
+ const lang = payload.lang;
813
+ const code = payload.code;
814
+ const args = payload.args || [];
815
+ const tempDir = await fsp.mkdtemp(path.join(WORKSPACE, "lc-ptc-"));
816
+ try {
817
+ const argText = args.map(quote).join(" ");
818
+ async function writeAndRun(fileName, command) {
819
+ const filePath = path.join(tempDir, fileName);
820
+ await fsp.writeFile(filePath, code, "utf8");
821
+ return formatRun(await run(command(filePath, argText), undefined, []));
822
+ }
823
+ if (lang === "py" || lang === "python") {
824
+ return writeAndRun("main.py", (filePath, argText) => "python3 " + quote(filePath) + " " + argText);
825
+ }
826
+ if (lang === "js" || lang === "javascript") {
827
+ return writeAndRun("main.js", (filePath, argText) => "node " + quote(filePath) + " " + argText);
828
+ }
829
+ if (lang === "ts" || lang === "typescript") {
830
+ return writeAndRun("main.ts", (filePath, argText) => "npx --no-install tsx " + quote(filePath) + " " + argText);
831
+ }
832
+ if (lang === "php") {
833
+ return writeAndRun("main.php", (filePath, argText) => "php " + quote(filePath) + " " + argText);
834
+ }
835
+ if (lang === "go") {
836
+ return writeAndRun("main.go", (filePath, argText) => "go run " + quote(filePath) + " " + argText);
837
+ }
838
+ if (lang === "rs") {
839
+ return writeAndRun("main.rs", (filePath, argText) => {
840
+ const binary = path.join(tempDir, "main-rs");
841
+ return "rustc " + quote(filePath) + " -o " + quote(binary) + " && " + quote(binary) + " " + argText;
842
+ });
843
+ }
844
+ if (lang === "c") {
845
+ return writeAndRun("main.c", (filePath, argText) => {
846
+ const binary = path.join(tempDir, "main-c");
847
+ return "cc " + quote(filePath) + " -o " + quote(binary) + " && " + quote(binary) + " " + argText;
848
+ });
849
+ }
850
+ if (lang === "cpp") {
851
+ return writeAndRun("main.cpp", (filePath, argText) => {
852
+ const binary = path.join(tempDir, "main-cpp");
853
+ return "c++ " + quote(filePath) + " -o " + quote(binary) + " && " + quote(binary) + " " + argText;
854
+ });
855
+ }
856
+ if (lang === "java") {
857
+ return writeAndRun("Main.java", (filePath, argText) => "javac " + quote(filePath) + " && java -cp " + quote(tempDir) + " Main " + argText);
858
+ }
859
+ if (lang === "r") {
860
+ return writeAndRun("main.R", (filePath, argText) => "Rscript " + quote(filePath) + " " + argText);
861
+ }
862
+ if (lang === "d") {
863
+ return writeAndRun("main.d", (filePath, argText) => {
864
+ const binary = path.join(tempDir, "main-d");
865
+ return "dmd " + quote(filePath) + " -of=" + quote(binary) + " && " + quote(binary) + " " + argText;
866
+ });
867
+ }
868
+ if (lang === "f90") {
869
+ return writeAndRun("main.f90", (filePath, argText) => {
870
+ const binary = path.join(tempDir, "main-f90");
871
+ return "gfortran " + quote(filePath) + " -o " + quote(binary) + " && " + quote(binary) + " " + argText;
872
+ });
873
+ }
874
+ if (lang === "bash" || lang === "sh") {
875
+ return formatRun(await run(code, undefined, args));
876
+ }
877
+ throw new Error("Unsupported Cloudflare sandbox runtime: " + lang);
878
+ } finally {
879
+ await fsp.rm(tempDir, { recursive: true, force: true });
880
+ }
881
+ }
882
+
883
+ async function read_file(payload) {
884
+ const resolved = resolvePath(payload.file_path);
885
+ return lineWindow(await fsp.readFile(resolved, "utf8"), payload.offset, payload.limit);
886
+ }
887
+
888
+ async function write_file(payload) {
889
+ assertWritable("write_file");
890
+ const resolved = resolvePath(payload.file_path);
891
+ await fsp.mkdir(path.dirname(resolved), { recursive: true });
892
+ const existed = fs.existsSync(resolved);
893
+ await fsp.writeFile(resolved, payload.content, "utf8");
894
+ return (existed ? "Overwrote " : "Created ") + resolved + " (" + payload.content.length + " chars).";
895
+ }
896
+
897
+ async function edit_file(payload) {
898
+ assertWritable("edit_file");
899
+ const resolved = resolvePath(payload.file_path);
900
+ const edits = payload.edits || [{ old_text: payload.old_text, new_text: payload.new_text }];
901
+ let content = await fsp.readFile(resolved, "utf8");
902
+ for (const edit of edits) {
903
+ const oldText = edit.old_text || "";
904
+ const newText = edit.new_text || "";
905
+ if (oldText === "" || content.split(oldText).length - 1 !== 1) {
906
+ throw new Error("Could not locate old_text exactly once in " + payload.file_path);
907
+ }
908
+ content = content.replace(oldText, newText);
909
+ }
910
+ await fsp.writeFile(resolved, content, "utf8");
911
+ return "Applied " + edits.length + " edit(s) to " + resolved + ".";
912
+ }
913
+
914
+ async function list_directory(payload) {
915
+ const resolved = resolvePath(payload.path || ".");
916
+ const entries = await fsp.readdir(resolved, { withFileTypes: true });
917
+ const lines = entries
918
+ .sort((a, b) => a.name.localeCompare(b.name))
919
+ .map((entry) => (entry.isDirectory() ? "dir" : "file") + "\\t" + entry.name);
920
+ return lines.join("\\n") || "Directory is empty.";
921
+ }
922
+
923
+ async function grep_search(payload) {
924
+ const root = resolvePath(payload.path || ".");
925
+ const regex = new RegExp(payload.pattern);
926
+ const maxResults = payload.max_results || 200;
927
+ const out = [];
928
+ await walkFiles(root, async (filePath) => {
929
+ if (out.length >= maxResults) return;
930
+ const relative = path.relative(root, filePath);
931
+ if (payload.glob && !globMatch(relative, payload.glob)) return;
932
+ let text = "";
933
+ try {
934
+ text = await fsp.readFile(filePath, "utf8");
935
+ } catch {
936
+ return;
937
+ }
938
+ text.split("\\n").forEach((line, index) => {
939
+ if (out.length < maxResults && regex.test(line)) {
940
+ out.push(filePath + ":" + (index + 1) + ":" + line);
941
+ }
942
+ });
943
+ });
944
+ return out.join("\\n") || "No matches found.";
945
+ }
946
+
947
+ async function glob_search(payload) {
948
+ const root = resolvePath(payload.path || ".");
949
+ const maxResults = payload.max_results || 200;
950
+ const out = [];
951
+ await walkFiles(root, async (filePath) => {
952
+ if (out.length >= maxResults) return;
953
+ const relative = path.relative(root, filePath);
954
+ if (globMatch(relative, payload.pattern)) out.push(filePath);
955
+ });
956
+ return out.join("\\n") || "No files found.";
957
+ }
958
+
959
+ async function compile_check(payload) {
960
+ const [kind, detected, reason] = await detectCompileCommand();
961
+ const command = payload.command || detected;
962
+ if (!command) {
963
+ return "compile_check: " + reason + ". Pass an explicit command to override.";
964
+ }
965
+ const result = await run(command, payload.timeout_ms);
966
+ const status = result.exit_code === 0 ? "PASSED" : "FAILED";
967
+ return "compile_check (" + kind + ") " + status + " via " + command + "\\n\\nstdout:\\n" + result.stdout + "\\nstderr:\\n" + result.stderr + "\\nworking_directory: " + WORKSPACE + "\\nreason: " + reason;
968
+ }
969
+
970
+ const TOOLS = {
971
+ bash_tool,
972
+ execute_code,
973
+ read_file,
974
+ write_file,
975
+ edit_file,
976
+ list_directory,
977
+ grep_search,
978
+ glob_search,
979
+ compile_check,
980
+ };
981
+
982
+ async function main() {
983
+ const name = process.argv[2];
984
+ const payload = JSON.parse(process.argv[3] || "{}");
985
+ if (!TOOLS[name]) throw new Error("Unknown tool: " + name);
986
+ const result = await TOOLS[name](payload);
987
+ process.stdout.write(typeof result === "string" ? result : JSON.stringify(result));
988
+ }
989
+
990
+ main().catch((error) => {
991
+ console.error(error && error.stack ? error.stack : String(error));
992
+ process.exit(1);
993
+ });
994
+ `.trim();
995
+ }
996
+
997
+ function createPythonProgram(
998
+ userCode: string,
999
+ toolDefs: t.LCTool[],
1000
+ config: t.CloudflareSandboxExecutionConfig,
1001
+ workspaceRoot: string
1002
+ ): string {
1003
+ const aliases = toolDefs
1004
+ .map((def) => {
1005
+ const pythonName = normalizeToPythonIdentifier(def.name);
1006
+ return NATIVE_TOOL_NAMES.has(def.name) && pythonName !== def.name
1007
+ ? `${pythonName} = globals()[${JSON.stringify(def.name)}]`
1008
+ : '';
1009
+ })
1010
+ .filter(Boolean)
1011
+ .join('\n');
1012
+ return `${createPythonNativeToolSource(config, workspaceRoot)}
1013
+ ${aliases}
1014
+
1015
+ async def __lc_user_main__():
1016
+ ${indent(userCode)}
1017
+
1018
+ asyncio.run(__lc_user_main__())
1019
+ `;
1020
+ }
1021
+
1022
+ function createBashProgram(
1023
+ userCode: string,
1024
+ toolDefs: t.LCTool[],
1025
+ config: t.CloudflareSandboxExecutionConfig,
1026
+ workspaceRoot: string
1027
+ ): string {
1028
+ const helper = createNodeNativeToolSource(config, workspaceRoot);
1029
+ const functions = toolDefs
1030
+ .map((def) => {
1031
+ const bashName = normalizeToBashIdentifier(def.name);
1032
+ if (!NATIVE_TOOL_NAMES.has(def.name)) {
1033
+ return '';
1034
+ }
1035
+ return `${bashName}() { node "$__LC_TOOL_HELPER" ${JSON.stringify(def.name)} "$1"; }`;
1036
+ })
1037
+ .filter(Boolean)
1038
+ .join('\n');
1039
+ return `
1040
+ set -euo pipefail
1041
+ command -v node >/dev/null 2>&1 || { echo "Cloudflare programmatic tool calling requires node in the sandbox image." >&2; exit 127; }
1042
+ __LC_TOOL_HELPER="$(mktemp /tmp/lc-tools.XXXXXX.js)"
1043
+ cat > "$__LC_TOOL_HELPER" <<'JS'
1044
+ ${helper}
1045
+ JS
1046
+ trap 'rm -f "$__LC_TOOL_HELPER"' EXIT
1047
+ ${functions}
1048
+ ${userCode}
1049
+ `.trim();
1050
+ }
1051
+
1052
+ async function runProgrammatic(args: {
1053
+ params: ProgrammaticParams;
1054
+ config?: { toolCall?: unknown };
1055
+ cloudflareConfig: t.CloudflareSandboxExecutionConfig;
1056
+ runtime: 'python' | 'bash';
1057
+ }): Promise<[string, t.ProgrammaticExecutionArtifact]> {
1058
+ const toolCall = (args.config?.toolCall ??
1059
+ {}) as Partial<t.ProgrammaticCache>;
1060
+ const toolDefs = toolCall.toolDefs ?? [];
1061
+ const effectiveTools = filterNativeTools(
1062
+ toolDefs,
1063
+ args.params.code,
1064
+ args.runtime
1065
+ );
1066
+ const timeoutMs = clampExecutionTimeout(
1067
+ args.params.timeout,
1068
+ args.cloudflareConfig.timeoutMs
1069
+ );
1070
+ const workspaceRoot = getCloudflareWorkspaceRoot(args.cloudflareConfig);
1071
+ let result: Awaited<ReturnType<typeof executeCloudflareCode>>;
1072
+
1073
+ if (args.runtime === 'bash') {
1074
+ await validateCloudflareBashCommand(args.params.code, [], {
1075
+ ...args.cloudflareConfig,
1076
+ timeoutMs,
1077
+ });
1078
+ result = await executeGeneratedCloudflareBash(
1079
+ createBashProgram(
1080
+ args.params.code,
1081
+ effectiveTools,
1082
+ args.cloudflareConfig,
1083
+ workspaceRoot
1084
+ ),
1085
+ { ...args.cloudflareConfig, timeoutMs }
1086
+ );
1087
+ } else {
1088
+ result = await executeCloudflareCode(
1089
+ {
1090
+ lang: 'py',
1091
+ code: createPythonProgram(
1092
+ args.params.code,
1093
+ effectiveTools,
1094
+ args.cloudflareConfig,
1095
+ workspaceRoot
1096
+ ),
1097
+ },
1098
+ { ...args.cloudflareConfig, timeoutMs }
1099
+ );
1100
+ }
1101
+
1102
+ if (result.exitCode !== 0 || result.timedOut) {
1103
+ throw new Error(
1104
+ result.stderr !== ''
1105
+ ? result.stderr
1106
+ : `Cloudflare ${args.runtime} programmatic execution exited with code ${result.exitCode ?? 'unknown'}`
1107
+ );
1108
+ }
1109
+
1110
+ return formatCompletedResponse({
1111
+ status: 'completed',
1112
+ session_id: 'cloudflare-sandbox',
1113
+ stdout: result.stdout,
1114
+ stderr: result.stderr,
1115
+ files: [],
1116
+ });
1117
+ }
1118
+
1119
+ export function createCloudflareProgrammaticToolCallingTool(
1120
+ cloudflareConfig: t.CloudflareSandboxExecutionConfig
1121
+ ): DynamicStructuredTool {
1122
+ return tool(
1123
+ async (rawParams, config) => {
1124
+ const params = rawParams as ProgrammaticParams;
1125
+ return runProgrammatic({
1126
+ params,
1127
+ config,
1128
+ cloudflareConfig,
1129
+ runtime: resolveRuntime(params),
1130
+ });
1131
+ },
1132
+ {
1133
+ name: ProgrammaticToolCallingName,
1134
+ description: `${ProgrammaticToolCallingDescription}\n\nCloudflare Sandbox engine: exposes the built-in coding tools inside the sandbox process. Non-coding host tools are not callable from this in-sandbox programmatic runner.`,
1135
+ schema: createCloudflareProgrammaticToolCallingSchema(cloudflareConfig),
1136
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
1137
+ }
1138
+ );
1139
+ }
1140
+
1141
+ export function createCloudflareBashProgrammaticToolCallingTool(
1142
+ cloudflareConfig: t.CloudflareSandboxExecutionConfig
1143
+ ): DynamicStructuredTool {
1144
+ return tool(
1145
+ async (rawParams, config) => {
1146
+ const params = rawParams as ProgrammaticParams;
1147
+ return runProgrammatic({
1148
+ params,
1149
+ config,
1150
+ cloudflareConfig,
1151
+ runtime: 'bash',
1152
+ });
1153
+ },
1154
+ {
1155
+ name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
1156
+ description: `${BashProgrammaticToolCallingDescription}\n\nCloudflare Sandbox engine: exposes the built-in coding tools as bash functions inside the sandbox process. Non-coding host tools are not callable from this in-sandbox programmatic runner.`,
1157
+ schema:
1158
+ createCloudflareBashProgrammaticToolCallingSchema(cloudflareConfig),
1159
+ responseFormat: Constants.CONTENT_AND_ARTIFACT,
1160
+ }
1161
+ );
1162
+ }