@librechat/agents 3.2.39 → 3.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +4 -3
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +2 -2
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +11 -11
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +11 -11
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +4 -3
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +2 -2
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +11 -11
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +11 -11
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/types/tools/ReadFile.d.ts +4 -4
- package/package.json +1 -1
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +12 -12
- package/src/hooks/createWorkspacePolicyHook.ts +7 -6
- package/src/tools/ReadFile.ts +2 -2
- package/src/tools/__tests__/LocalExecutionTools.test.ts +25 -25
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +5 -5
- package/src/tools/__tests__/ReadFile.test.ts +3 -3
- package/src/tools/__tests__/ToolNode.session.test.ts +2 -2
- package/src/tools/__tests__/workspaceSeam.test.ts +2 -2
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +11 -11
- package/src/tools/local/LocalCodingTools.ts +14 -14
|
@@ -75,10 +75,11 @@ function extractCompileCheckPaths(input) {
|
|
|
75
75
|
for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) out.push(expandHomeRelative(match[1]));
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
|
+
const extractPath = (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [];
|
|
78
79
|
const DEFAULT_EXTRACTORS = {
|
|
79
|
-
["read_file"]:
|
|
80
|
-
["write_file"]:
|
|
81
|
-
["edit_file"]:
|
|
80
|
+
["read_file"]: extractPath,
|
|
81
|
+
["write_file"]: extractPath,
|
|
82
|
+
["edit_file"]: extractPath,
|
|
82
83
|
["grep_search"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
|
|
83
84
|
["glob_search"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
|
|
84
85
|
["list_directory"]: (i) => typeof i.path === "string" && i.path !== "" ? [i.path] : [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createWorkspacePolicyHook.cjs","names":[],"sources":["../../../src/hooks/createWorkspacePolicyHook.ts"],"sourcesContent":["/**\n * Workspace boundary policy as a `PreToolUse` hook.\n *\n * Local-engine file tools enforce a hard workspace boundary at the\n * tool implementation layer (`resolveWorkspacePathSafe`). This hook\n * adds a complementary, host-controlled layer on top that uses the\n * standard PreToolUse / HITL machinery to *negotiate* access to\n * paths outside the workspace — instead of just throwing.\n *\n * The host opts in by registering this hook on a `HookRegistry`; the\n * hook inspects each tool call's input, extracts the file paths it\n * mentions via per-tool extractors, and returns:\n *\n * - `allow` — every path is inside `workspace.root`\n * (or `additionalRoots`)\n * - `deny` — at least one path is outside, and the\n * configured outside-policy is `'deny'`\n * - `ask` — at least one path is outside, and the\n * outside-policy is `'ask'` (default).\n * When `humanInTheLoop.enabled` is true,\n * the existing PreToolUse `'ask'` flow\n * raises a tool_approval interrupt the\n * host UI can render. When HITL is off,\n * `'ask'` collapses to `deny` (matches\n * the rest of the SDK's default).\n *\n * Default per-tool path extractors cover the local-engine coding\n * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,\n * `glob_search`, `list_directory`, `compile_check`). The host can\n * override or extend via `pathExtractors`. Bash/code paths are not\n * extracted by default — bash command parsing is its own concern, and\n * the existing `bashAst` validator + sandbox-runtime fs allowlist are\n * the right gates for those.\n *\n * Important: this hook does NOT replace `resolveWorkspacePathSafe`.\n * Even if the hook returns `allow`, the file tool still enforces its\n * own clamp unless `workspace.allowReadOutside` /\n * `workspace.allowWriteOutside` (or the legacy\n * `allowOutsideWorkspace`) is set. The recommended composition for\n * \"ask the user\" semantics is:\n *\n * workspace: {\n * root,\n * allowReadOutside: true,\n * allowWriteOutside: true,\n * },\n * // …with the hook installed and humanInTheLoop.enabled = true.\n */\n\nimport { homedir } from 'os';\nimport { realpath } from 'fs/promises';\nimport { isAbsolute, relative, resolve } from 'path';\nimport type {\n HookCallback,\n PreToolUseHookInput,\n PreToolUseHookOutput,\n ToolDecision,\n} from './types';\nimport { Constants } from '@/common';\n\n/**\n * What to do when a tool call references a path outside the workspace.\n *\n * - `'ask'` : default. Raise a PreToolUse `ask` (host UI prompts\n * via the HITL interrupt path).\n * - `'allow'` : let the call through (use the existing tool clamp\n * to actually enforce — the hook is purely advisory).\n * - `'deny'` : block the call with an error ToolMessage.\n */\nexport type OutsideAccessPolicy = 'ask' | 'allow' | 'deny';\n\nexport interface WorkspacePolicyConfig {\n /** Canonical workspace root. Required. */\n root: string;\n /** Sibling roots that count as inside-workspace. */\n additionalRoots?: readonly string[];\n /** Policy applied to read-only file tools. Defaults to `'ask'`. */\n outsideRead?: OutsideAccessPolicy;\n /** Policy applied to write-shaped file tools. Defaults to `'ask'`. */\n outsideWrite?: OutsideAccessPolicy;\n /**\n * Optional reason template surfaced in the `ask`/`deny` decision.\n * Supports `{tool}` and `{paths}` substitution.\n */\n reason?: string;\n /**\n * Per-tool path extractors. Defaults cover the local-engine coding\n * suite. Returning an empty array opts that tool out of policy.\n */\n pathExtractors?: Record<string, PathExtractor>;\n}\n\nexport type PathExtractor = (\n toolInput: Record<string, unknown>\n) => readonly string[];\n\nconst READ_TOOLS = new Set<string>([\n Constants.READ_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n]);\n\nconst WRITE_TOOLS = new Set<string>([\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n]);\n\n/**\n * Best-effort extractor for `compile_check` — pulls absolute and `~/`\n * path tokens out of the `command` string so the workspace boundary\n * sees them. Without this, a model could ship `command: 'cat\n * /etc/passwd'` and the policy hook would short-circuit to `allow`\n * (Codex P1 #26 — the prior `() => []` made the hook a no-op for\n * compile_check). Conservative by design:\n *\n * - Matches `/foo`, `~/foo`, `$HOME/foo`, `${HOME}/foo` followed by\n * non-shell-special chars. Stops at whitespace, quotes, redirect\n * operators, pipes, semicolons.\n * - Strips a leading `--flag=` so `--out=/etc/foo` extracts as\n * `/etc/foo` (the path the agent's actually trying to write).\n * - Misses relative paths (intended — those resolve under cwd\n * anyway), and shell-substituted paths whose final form isn't\n * visible at extract time. Hosts that need bulletproof gating\n * should pair this with a `bash_tool`-level policy.\n */\n// `[\"']?` slots before AND after the captured path cover quoted\n// forms like `cat \"/etc/passwd\"` and `--out='/tmp/x'`. Codex P1 #31\n// — the previous regex only matched unquoted tokens, so a model\n// could trivially bypass the workspace policy by quoting any\n// destination path. The path content character class still excludes\n// quotes/whitespace/shell-specials so we don't over-extract; that's\n// the defensive trade we want for fallback-grep style matching.\n//\n// The `\\.\\.(?:\\/[^…]*)?` alternation covers parent-traversal forms\n// (`..`, `../secrets.txt`, `../foo/bar`). Without it, a model could\n// exfiltrate parent-directory files via `cat ../secrets` and the\n// hook would short-circuit to `allow` because the extractor saw no\n// \"absolute\" token. The boundary check at the call site resolves\n// non-absolute extracted tokens against `root`, so `../secrets`\n// becomes `<parent-of-workspace>/secrets` which the boundary then\n// correctly flags as outside. Codex P2 #35.\nconst PATH_TOKEN =\n /(?:^|[\\s=])(?:--[^\\s=]+=)?[\"']?(\\/[^\\s'\"|;&<>()`]+|~\\/[^\\s'\"|;&<>()`]+|\\$\\{?HOME\\}?\\/[^\\s'\"|;&<>()`]+|\\.\\.(?:\\/[^\\s'\"|;&<>()`]*)?)[\"']?/g;\n// Back-compat alias kept for any downstream import.\nconst ABSOLUTE_PATH_TOKEN = PATH_TOKEN;\nfunction expandHomeRelative(token: string): string {\n // Expand ~/foo and $HOME/foo and ${HOME}/foo to absolute. The\n // workspace boundary check resolves non-absolute paths against the\n // workspace root, which would silently treat `~/secret` as\n // `<workspace>/~/secret` — exactly the bypass the codex flagged.\n const home = homedir();\n if (token.startsWith('~/')) return `${home}/${token.slice(2)}`;\n if (token.startsWith('${HOME}/')) return `${home}/${token.slice(8)}`;\n if (token.startsWith('$HOME/')) return `${home}/${token.slice(6)}`;\n return token;\n}\nfunction extractCompileCheckPaths(input: Record<string, unknown>): string[] {\n const command = typeof input.command === 'string' ? input.command : '';\n if (command === '') return [];\n const out: string[] = [];\n for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) {\n out.push(expandHomeRelative(match[1]));\n }\n return out;\n}\n\nconst DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {\n [Constants.READ_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.WRITE_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.EDIT_FILE]: (i) =>\n typeof i.file_path === 'string' ? [i.file_path] : [],\n [Constants.GREP_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.GLOB_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.LIST_DIRECTORY]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.COMPILE_CHECK]: extractCompileCheckPaths,\n};\n\nfunction isInsideAnyRoot(absolutePath: string, roots: string[]): boolean {\n for (const root of roots) {\n if (absolutePath === root) return true;\n const rel = relative(root, absolutePath);\n if (!rel.startsWith('..') && !isAbsolute(rel)) return true;\n }\n return false;\n}\n\n/**\n * Symlink-aware variant: realpaths the candidate AND the roots before\n * comparing. Without this, a symlink inside the workspace pointing\n * outside (e.g. `workspace/link → /etc/passwd`) compares as\n * \"in-workspace\" lexically, but actually grants the agent reach\n * outside the boundary. Critical when this hook is the primary gate\n * (i.e. the host opted into `workspace.allowReadOutside: true` /\n * `allowWriteOutside: true` so the file tools' own clamp is off).\n *\n * Handles paths that don't yet exist (e.g. `write_file` to a brand\n * new path) by walking up to the nearest existing ancestor and\n * realpathing that, then re-attaching the unresolved suffix. Mirrors\n * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.\n */\nasync function realpathOrSelf(absolutePath: string): Promise<string> {\n try {\n return await realpath(absolutePath);\n } catch {\n return absolutePath;\n }\n}\n\nasync function realpathOfPathOrAncestor(absolutePath: string): Promise<string> {\n let current = absolutePath;\n let suffix = '';\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n try {\n const real = await realpath(current);\n return suffix === '' ? real : resolve(real, suffix);\n } catch {\n const parent = resolve(current, '..');\n if (parent === current) {\n return absolutePath;\n }\n const base = current.slice(parent.length + 1);\n suffix = suffix === '' ? base : `${base}/${suffix}`;\n current = parent;\n }\n }\n}\n\nasync function isInsideAnyRootRealpath(\n absolutePath: string,\n realRoots: readonly string[]\n): Promise<boolean> {\n const real = await realpathOfPathOrAncestor(absolutePath);\n return isInsideAnyRoot(real, [...realRoots]);\n}\n\nfunction formatReason(\n template: string | undefined,\n toolName: string,\n outsidePaths: readonly string[]\n): string {\n const fallback = `Tool \"${toolName}\" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(', ')}`;\n if (template == null) return fallback;\n return template\n .replace(/\\{tool\\}/g, toolName)\n .replace(/\\{paths\\}/g, outsidePaths.join(', '));\n}\n\n/**\n * Build a `PreToolUse` callback that enforces the workspace policy.\n * Register it on a `HookRegistry`:\n *\n * ```ts\n * registry.register('PreToolUse', {\n * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],\n * });\n * ```\n *\n * The hook is composable with `createToolPolicyHook` — register both;\n * `executeHooks` precedence (`deny > ask > allow`) sorts out which\n * decision wins per call.\n */\nexport function createWorkspacePolicyHook(\n config: WorkspacePolicyConfig\n): HookCallback<'PreToolUse'> {\n const root = resolve(config.root);\n // Relative `additionalRoots` entries are anchored to `root` so a\n // monorepo config like `additionalRoots: ['../shared']` resolves\n // to a sibling of `root`, not of process.cwd. Matches\n // `getWorkspaceRoots` in LocalExecutionEngine.\n const additionalRoots = (config.additionalRoots ?? []).map((p) =>\n isAbsolute(p) ? resolve(p) : resolve(root, p)\n );\n const allRoots = [root, ...additionalRoots];\n\n // Pre-realpath the roots once at construction — these are stable\n // per Run. The candidate paths get realpath'd lazily inside the\n // hook callback. Cached so the per-call cost is just one realpath.\n let realRootsPromise: Promise<string[]> | undefined;\n const getRealRoots = (): Promise<string[]> => {\n if (realRootsPromise == null) {\n realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));\n }\n return realRootsPromise;\n };\n\n const readPolicy: OutsideAccessPolicy = config.outsideRead ?? 'ask';\n const writePolicy: OutsideAccessPolicy = config.outsideWrite ?? 'ask';\n\n const extractors: Record<string, PathExtractor | undefined> = {\n ...DEFAULT_EXTRACTORS,\n ...(config.pathExtractors ?? {}),\n };\n\n /** Unknown tools are treated as writes (the stricter policy). */\n const resolvePolicy = (toolName: string): OutsideAccessPolicy => {\n if (WRITE_TOOLS.has(toolName)) {\n return writePolicy;\n }\n if (READ_TOOLS.has(toolName)) {\n return readPolicy;\n }\n return writePolicy;\n };\n\n return async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {\n const extractor = extractors[input.toolName];\n if (extractor == null) return { decision: 'allow' };\n\n const paths = extractor(input.toolInput);\n if (paths.length === 0) return { decision: 'allow' };\n\n // Two-stage check:\n // 1. Lexical fast path — anything that's lexically inside the\n // workspace AND doesn't get redirected by realpath stays\n // allow-able without paying the realpath cost on every call.\n // 2. For paths that look outside lexically OR look inside but\n // may have been routed through a symlink, realpath both the\n // candidate and the roots and compare. This catches the\n // `workspace/link → /etc/passwd` escape that lexical-only\n // checks miss.\n const outside: string[] = [];\n const realRoots = await getRealRoots();\n for (const p of paths) {\n const abs = isAbsolute(p) ? resolve(p) : resolve(root, p);\n // Realpath is the source of truth — it catches both the\n // symlink-escape case (lexically-inside path that resolves\n // outside) and the alternate-mount case (lexically-outside\n // path that resolves back inside the workspace). The lexical\n // check alone gives the wrong answer for either, so we don't\n // bother computing it.\n const realInside = await isInsideAnyRootRealpath(abs, realRoots);\n if (!realInside) {\n outside.push(p);\n }\n }\n if (outside.length === 0) return { decision: 'allow' };\n\n const policy = resolvePolicy(input.toolName);\n if (policy === 'allow') return { decision: 'allow' };\n\n const decision: ToolDecision = policy === 'deny' ? 'deny' : 'ask';\n return {\n decision,\n reason: formatReason(config.reason, input.toolName, outside),\n ...(decision === 'ask'\n ? { allowedDecisions: ['approve', 'reject'] as const }\n : {}),\n };\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createWorkspacePolicyHook.cjs","names":[],"sources":["../../../src/hooks/createWorkspacePolicyHook.ts"],"sourcesContent":["/**\n * Workspace boundary policy as a `PreToolUse` hook.\n *\n * Local-engine file tools enforce a hard workspace boundary at the\n * tool implementation layer (`resolveWorkspacePathSafe`). This hook\n * adds a complementary, host-controlled layer on top that uses the\n * standard PreToolUse / HITL machinery to *negotiate* access to\n * paths outside the workspace — instead of just throwing.\n *\n * The host opts in by registering this hook on a `HookRegistry`; the\n * hook inspects each tool call's input, extracts the file paths it\n * mentions via per-tool extractors, and returns:\n *\n * - `allow` — every path is inside `workspace.root`\n * (or `additionalRoots`)\n * - `deny` — at least one path is outside, and the\n * configured outside-policy is `'deny'`\n * - `ask` — at least one path is outside, and the\n * outside-policy is `'ask'` (default).\n * When `humanInTheLoop.enabled` is true,\n * the existing PreToolUse `'ask'` flow\n * raises a tool_approval interrupt the\n * host UI can render. When HITL is off,\n * `'ask'` collapses to `deny` (matches\n * the rest of the SDK's default).\n *\n * Default per-tool path extractors cover the local-engine coding\n * suite (`read_file`, `write_file`, `edit_file`, `grep_search`,\n * `glob_search`, `list_directory`, `compile_check`). The host can\n * override or extend via `pathExtractors`. Bash/code paths are not\n * extracted by default — bash command parsing is its own concern, and\n * the existing `bashAst` validator + sandbox-runtime fs allowlist are\n * the right gates for those.\n *\n * Important: this hook does NOT replace `resolveWorkspacePathSafe`.\n * Even if the hook returns `allow`, the file tool still enforces its\n * own clamp unless `workspace.allowReadOutside` /\n * `workspace.allowWriteOutside` (or the legacy\n * `allowOutsideWorkspace`) is set. The recommended composition for\n * \"ask the user\" semantics is:\n *\n * workspace: {\n * root,\n * allowReadOutside: true,\n * allowWriteOutside: true,\n * },\n * // …with the hook installed and humanInTheLoop.enabled = true.\n */\n\nimport { homedir } from 'os';\nimport { realpath } from 'fs/promises';\nimport { isAbsolute, relative, resolve } from 'path';\nimport type {\n HookCallback,\n PreToolUseHookInput,\n PreToolUseHookOutput,\n ToolDecision,\n} from './types';\nimport { Constants } from '@/common';\n\n/**\n * What to do when a tool call references a path outside the workspace.\n *\n * - `'ask'` : default. Raise a PreToolUse `ask` (host UI prompts\n * via the HITL interrupt path).\n * - `'allow'` : let the call through (use the existing tool clamp\n * to actually enforce — the hook is purely advisory).\n * - `'deny'` : block the call with an error ToolMessage.\n */\nexport type OutsideAccessPolicy = 'ask' | 'allow' | 'deny';\n\nexport interface WorkspacePolicyConfig {\n /** Canonical workspace root. Required. */\n root: string;\n /** Sibling roots that count as inside-workspace. */\n additionalRoots?: readonly string[];\n /** Policy applied to read-only file tools. Defaults to `'ask'`. */\n outsideRead?: OutsideAccessPolicy;\n /** Policy applied to write-shaped file tools. Defaults to `'ask'`. */\n outsideWrite?: OutsideAccessPolicy;\n /**\n * Optional reason template surfaced in the `ask`/`deny` decision.\n * Supports `{tool}` and `{paths}` substitution.\n */\n reason?: string;\n /**\n * Per-tool path extractors. Defaults cover the local-engine coding\n * suite. Returning an empty array opts that tool out of policy.\n */\n pathExtractors?: Record<string, PathExtractor>;\n}\n\nexport type PathExtractor = (\n toolInput: Record<string, unknown>\n) => readonly string[];\n\nconst READ_TOOLS = new Set<string>([\n Constants.READ_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n]);\n\nconst WRITE_TOOLS = new Set<string>([\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n]);\n\n/**\n * Best-effort extractor for `compile_check` — pulls absolute and `~/`\n * path tokens out of the `command` string so the workspace boundary\n * sees them. Without this, a model could ship `command: 'cat\n * /etc/passwd'` and the policy hook would short-circuit to `allow`\n * (Codex P1 #26 — the prior `() => []` made the hook a no-op for\n * compile_check). Conservative by design:\n *\n * - Matches `/foo`, `~/foo`, `$HOME/foo`, `${HOME}/foo` followed by\n * non-shell-special chars. Stops at whitespace, quotes, redirect\n * operators, pipes, semicolons.\n * - Strips a leading `--flag=` so `--out=/etc/foo` extracts as\n * `/etc/foo` (the path the agent's actually trying to write).\n * - Misses relative paths (intended — those resolve under cwd\n * anyway), and shell-substituted paths whose final form isn't\n * visible at extract time. Hosts that need bulletproof gating\n * should pair this with a `bash_tool`-level policy.\n */\n// `[\"']?` slots before AND after the captured path cover quoted\n// forms like `cat \"/etc/passwd\"` and `--out='/tmp/x'`. Codex P1 #31\n// — the previous regex only matched unquoted tokens, so a model\n// could trivially bypass the workspace policy by quoting any\n// destination path. The path content character class still excludes\n// quotes/whitespace/shell-specials so we don't over-extract; that's\n// the defensive trade we want for fallback-grep style matching.\n//\n// The `\\.\\.(?:\\/[^…]*)?` alternation covers parent-traversal forms\n// (`..`, `../secrets.txt`, `../foo/bar`). Without it, a model could\n// exfiltrate parent-directory files via `cat ../secrets` and the\n// hook would short-circuit to `allow` because the extractor saw no\n// \"absolute\" token. The boundary check at the call site resolves\n// non-absolute extracted tokens against `root`, so `../secrets`\n// becomes `<parent-of-workspace>/secrets` which the boundary then\n// correctly flags as outside. Codex P2 #35.\nconst PATH_TOKEN =\n /(?:^|[\\s=])(?:--[^\\s=]+=)?[\"']?(\\/[^\\s'\"|;&<>()`]+|~\\/[^\\s'\"|;&<>()`]+|\\$\\{?HOME\\}?\\/[^\\s'\"|;&<>()`]+|\\.\\.(?:\\/[^\\s'\"|;&<>()`]*)?)[\"']?/g;\n// Back-compat alias kept for any downstream import.\nconst ABSOLUTE_PATH_TOKEN = PATH_TOKEN;\nfunction expandHomeRelative(token: string): string {\n // Expand ~/foo and $HOME/foo and ${HOME}/foo to absolute. The\n // workspace boundary check resolves non-absolute paths against the\n // workspace root, which would silently treat `~/secret` as\n // `<workspace>/~/secret` — exactly the bypass the codex flagged.\n const home = homedir();\n if (token.startsWith('~/')) return `${home}/${token.slice(2)}`;\n if (token.startsWith('${HOME}/')) return `${home}/${token.slice(8)}`;\n if (token.startsWith('$HOME/')) return `${home}/${token.slice(6)}`;\n return token;\n}\nfunction extractCompileCheckPaths(input: Record<string, unknown>): string[] {\n const command = typeof input.command === 'string' ? input.command : '';\n if (command === '') return [];\n const out: string[] = [];\n for (const match of command.matchAll(ABSOLUTE_PATH_TOKEN)) {\n out.push(expandHomeRelative(match[1]));\n }\n return out;\n}\n\n// All built-in coding tools take their file/dir target in `path`.\nconst extractPath: PathExtractor = (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [];\n\nconst DEFAULT_EXTRACTORS: Record<string, PathExtractor> = {\n [Constants.READ_FILE]: extractPath,\n [Constants.WRITE_FILE]: extractPath,\n [Constants.EDIT_FILE]: extractPath,\n [Constants.GREP_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.GLOB_SEARCH]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.LIST_DIRECTORY]: (i) =>\n typeof i.path === 'string' && i.path !== '' ? [i.path] : [],\n [Constants.COMPILE_CHECK]: extractCompileCheckPaths,\n};\n\nfunction isInsideAnyRoot(absolutePath: string, roots: string[]): boolean {\n for (const root of roots) {\n if (absolutePath === root) return true;\n const rel = relative(root, absolutePath);\n if (!rel.startsWith('..') && !isAbsolute(rel)) return true;\n }\n return false;\n}\n\n/**\n * Symlink-aware variant: realpaths the candidate AND the roots before\n * comparing. Without this, a symlink inside the workspace pointing\n * outside (e.g. `workspace/link → /etc/passwd`) compares as\n * \"in-workspace\" lexically, but actually grants the agent reach\n * outside the boundary. Critical when this hook is the primary gate\n * (i.e. the host opted into `workspace.allowReadOutside: true` /\n * `allowWriteOutside: true` so the file tools' own clamp is off).\n *\n * Handles paths that don't yet exist (e.g. `write_file` to a brand\n * new path) by walking up to the nearest existing ancestor and\n * realpathing that, then re-attaching the unresolved suffix. Mirrors\n * `resolveWorkspacePathSafe`'s approach in LocalExecutionEngine.\n */\nasync function realpathOrSelf(absolutePath: string): Promise<string> {\n try {\n return await realpath(absolutePath);\n } catch {\n return absolutePath;\n }\n}\n\nasync function realpathOfPathOrAncestor(absolutePath: string): Promise<string> {\n let current = absolutePath;\n let suffix = '';\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n try {\n const real = await realpath(current);\n return suffix === '' ? real : resolve(real, suffix);\n } catch {\n const parent = resolve(current, '..');\n if (parent === current) {\n return absolutePath;\n }\n const base = current.slice(parent.length + 1);\n suffix = suffix === '' ? base : `${base}/${suffix}`;\n current = parent;\n }\n }\n}\n\nasync function isInsideAnyRootRealpath(\n absolutePath: string,\n realRoots: readonly string[]\n): Promise<boolean> {\n const real = await realpathOfPathOrAncestor(absolutePath);\n return isInsideAnyRoot(real, [...realRoots]);\n}\n\nfunction formatReason(\n template: string | undefined,\n toolName: string,\n outsidePaths: readonly string[]\n): string {\n const fallback = `Tool \"${toolName}\" wants to touch ${outsidePaths.length} path(s) outside the workspace: ${outsidePaths.join(', ')}`;\n if (template == null) return fallback;\n return template\n .replace(/\\{tool\\}/g, toolName)\n .replace(/\\{paths\\}/g, outsidePaths.join(', '));\n}\n\n/**\n * Build a `PreToolUse` callback that enforces the workspace policy.\n * Register it on a `HookRegistry`:\n *\n * ```ts\n * registry.register('PreToolUse', {\n * hooks: [createWorkspacePolicyHook({ root, outsideWrite: 'ask' })],\n * });\n * ```\n *\n * The hook is composable with `createToolPolicyHook` — register both;\n * `executeHooks` precedence (`deny > ask > allow`) sorts out which\n * decision wins per call.\n */\nexport function createWorkspacePolicyHook(\n config: WorkspacePolicyConfig\n): HookCallback<'PreToolUse'> {\n const root = resolve(config.root);\n // Relative `additionalRoots` entries are anchored to `root` so a\n // monorepo config like `additionalRoots: ['../shared']` resolves\n // to a sibling of `root`, not of process.cwd. Matches\n // `getWorkspaceRoots` in LocalExecutionEngine.\n const additionalRoots = (config.additionalRoots ?? []).map((p) =>\n isAbsolute(p) ? resolve(p) : resolve(root, p)\n );\n const allRoots = [root, ...additionalRoots];\n\n // Pre-realpath the roots once at construction — these are stable\n // per Run. The candidate paths get realpath'd lazily inside the\n // hook callback. Cached so the per-call cost is just one realpath.\n let realRootsPromise: Promise<string[]> | undefined;\n const getRealRoots = (): Promise<string[]> => {\n if (realRootsPromise == null) {\n realRootsPromise = Promise.all(allRoots.map(realpathOrSelf));\n }\n return realRootsPromise;\n };\n\n const readPolicy: OutsideAccessPolicy = config.outsideRead ?? 'ask';\n const writePolicy: OutsideAccessPolicy = config.outsideWrite ?? 'ask';\n\n const extractors: Record<string, PathExtractor | undefined> = {\n ...DEFAULT_EXTRACTORS,\n ...(config.pathExtractors ?? {}),\n };\n\n /** Unknown tools are treated as writes (the stricter policy). */\n const resolvePolicy = (toolName: string): OutsideAccessPolicy => {\n if (WRITE_TOOLS.has(toolName)) {\n return writePolicy;\n }\n if (READ_TOOLS.has(toolName)) {\n return readPolicy;\n }\n return writePolicy;\n };\n\n return async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {\n const extractor = extractors[input.toolName];\n if (extractor == null) return { decision: 'allow' };\n\n const paths = extractor(input.toolInput);\n if (paths.length === 0) return { decision: 'allow' };\n\n // Two-stage check:\n // 1. Lexical fast path — anything that's lexically inside the\n // workspace AND doesn't get redirected by realpath stays\n // allow-able without paying the realpath cost on every call.\n // 2. For paths that look outside lexically OR look inside but\n // may have been routed through a symlink, realpath both the\n // candidate and the roots and compare. This catches the\n // `workspace/link → /etc/passwd` escape that lexical-only\n // checks miss.\n const outside: string[] = [];\n const realRoots = await getRealRoots();\n for (const p of paths) {\n const abs = isAbsolute(p) ? resolve(p) : resolve(root, p);\n // Realpath is the source of truth — it catches both the\n // symlink-escape case (lexically-inside path that resolves\n // outside) and the alternate-mount case (lexically-outside\n // path that resolves back inside the workspace). The lexical\n // check alone gives the wrong answer for either, so we don't\n // bother computing it.\n const realInside = await isInsideAnyRootRealpath(abs, realRoots);\n if (!realInside) {\n outside.push(p);\n }\n }\n if (outside.length === 0) return { decision: 'allow' };\n\n const policy = resolvePolicy(input.toolName);\n if (policy === 'allow') return { decision: 'allow' };\n\n const decision: ToolDecision = policy === 'deny' ? 'deny' : 'ask';\n return {\n decision,\n reason: formatReason(config.reason, input.toolName, outside),\n ...(decision === 'ask'\n ? { allowedDecisions: ['approve', 'reject'] as const }\n : {}),\n };\n };\n}\n"],"mappings":"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"}
|
|
@@ -19,11 +19,11 @@ CONSTRAINTS:
|
|
|
19
19
|
- Do not guess file paths. Use paths from the skill documentation or tool output.`;
|
|
20
20
|
const ReadFileToolSchema = {
|
|
21
21
|
type: "object",
|
|
22
|
-
properties: {
|
|
22
|
+
properties: { path: {
|
|
23
23
|
type: "string",
|
|
24
24
|
description: "Path to the file. For skill files: \"{skillName}/{path}\" (e.g. \"pdf-analyzer/src/utils.py\"). For code execution output: the path as returned by the execution tool."
|
|
25
25
|
} },
|
|
26
|
-
required: ["
|
|
26
|
+
required: ["path"]
|
|
27
27
|
};
|
|
28
28
|
const ReadFileToolDefinition = {
|
|
29
29
|
name: ReadFileToolName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReadFile.cjs","names":[],"sources":["../../../src/tools/ReadFile.ts"],"sourcesContent":["// src/tools/ReadFile.ts\nimport { Constants } from '@/common';\n\nexport const ReadFileToolName = Constants.READ_FILE;\n\nexport const ReadFileToolDescription = `Read the contents of a file. Returns text content with line numbers for easy reference.\n\nFor skill files, use the path format: {skillName}/{filePath} (e.g. \"pdf-analyzer/src/utils.py\", \"code-review/SKILL.md\").\n\nBEHAVIOR:\n- Text files: returned with numbered lines.\n- Images (png, jpeg, gif, webp): returned as visual content the model can see.\n- PDFs: returned as document content.\n- Other binary files: metadata returned with a note to use bash for processing.\n- Large files (>256KB text, >10MB binary): metadata only.\n- SKILL.md: returns the skill's instructions directly.\n\nCONSTRAINTS:\n- Only files from invoked skills or code execution output are accessible.\n- Do not guess file paths. Use paths from the skill documentation or tool output.`;\n\nexport const ReadFileToolSchema = {\n type: 'object',\n properties: {\n
|
|
1
|
+
{"version":3,"file":"ReadFile.cjs","names":[],"sources":["../../../src/tools/ReadFile.ts"],"sourcesContent":["// src/tools/ReadFile.ts\nimport { Constants } from '@/common';\n\nexport const ReadFileToolName = Constants.READ_FILE;\n\nexport const ReadFileToolDescription = `Read the contents of a file. Returns text content with line numbers for easy reference.\n\nFor skill files, use the path format: {skillName}/{filePath} (e.g. \"pdf-analyzer/src/utils.py\", \"code-review/SKILL.md\").\n\nBEHAVIOR:\n- Text files: returned with numbered lines.\n- Images (png, jpeg, gif, webp): returned as visual content the model can see.\n- PDFs: returned as document content.\n- Other binary files: metadata returned with a note to use bash for processing.\n- Large files (>256KB text, >10MB binary): metadata only.\n- SKILL.md: returns the skill's instructions directly.\n\nCONSTRAINTS:\n- Only files from invoked skills or code execution output are accessible.\n- Do not guess file paths. Use paths from the skill documentation or tool output.`;\n\nexport const ReadFileToolSchema = {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description:\n 'Path to the file. For skill files: \"{skillName}/{path}\" (e.g. \"pdf-analyzer/src/utils.py\"). For code execution output: the path as returned by the execution tool.',\n },\n },\n required: ['path'],\n} as const;\n\nexport const ReadFileToolDefinition = {\n name: ReadFileToolName,\n description: ReadFileToolDescription,\n parameters: ReadFileToolSchema,\n responseFormat: 'content_and_artifact' as const,\n} as const;\n"],"mappings":";;;AAGA,MAAa,mBAAA;AAEb,MAAa,0BAA0B;;;;;;;;;;;;;;;AAgBvC,MAAa,qBAAqB;CAChC,MAAM;CACN,YAAY,EACV,MAAM;EACJ,MAAM;EACN,aACE;CACJ,EACF;CACA,UAAU,CAAC,MAAM;AACnB;AAEA,MAAa,yBAAyB;CACpC,MAAM;CACN,aAAa;CACb,YAAY;CACZ,gBAAgB;AAClB"}
|
|
@@ -360,30 +360,30 @@ async def execute_code(lang, code, args=None):
|
|
|
360
360
|
finally:
|
|
361
361
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
362
362
|
|
|
363
|
-
async def read_file(
|
|
364
|
-
resolved = _resolve(
|
|
363
|
+
async def read_file(path, offset=None, limit=None):
|
|
364
|
+
resolved = _resolve(path)
|
|
365
365
|
with open(resolved, encoding="utf-8") as handle:
|
|
366
366
|
return _line_window(handle.read(), offset, limit)
|
|
367
367
|
|
|
368
|
-
async def write_file(
|
|
368
|
+
async def write_file(path, content):
|
|
369
369
|
_assert_writable("write_file")
|
|
370
|
-
resolved = _resolve(
|
|
370
|
+
resolved = _resolve(path)
|
|
371
371
|
os.makedirs(os.path.dirname(resolved), exist_ok=True)
|
|
372
372
|
existed = os.path.exists(resolved)
|
|
373
373
|
with open(resolved, "w", encoding="utf-8") as handle:
|
|
374
374
|
handle.write(content)
|
|
375
375
|
return f"{'Overwrote' if existed else 'Created'} {resolved} ({len(content)} chars)."
|
|
376
376
|
|
|
377
|
-
async def edit_file(
|
|
377
|
+
async def edit_file(path, old_text=None, new_text=None, edits=None):
|
|
378
378
|
_assert_writable("edit_file")
|
|
379
|
-
resolved = _resolve(
|
|
379
|
+
resolved = _resolve(path)
|
|
380
380
|
edits = edits or [{"old_text": old_text, "new_text": new_text}]
|
|
381
381
|
content = open(resolved, encoding="utf-8").read()
|
|
382
382
|
for edit in edits:
|
|
383
383
|
old = edit.get("old_text") or ""
|
|
384
384
|
new = edit.get("new_text") or ""
|
|
385
385
|
if content.count(old) != 1:
|
|
386
|
-
raise ValueError(f"Could not locate old_text exactly once in {
|
|
386
|
+
raise ValueError(f"Could not locate old_text exactly once in {path}")
|
|
387
387
|
content = content.replace(old, new, 1)
|
|
388
388
|
open(resolved, "w", encoding="utf-8").write(content)
|
|
389
389
|
return f"Applied {len(edits)} edit(s) to {resolved}."
|
|
@@ -759,13 +759,13 @@ async function execute_code(payload) {
|
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
async function read_file(payload) {
|
|
762
|
-
const resolved = resolvePath(payload.
|
|
762
|
+
const resolved = resolvePath(payload.path);
|
|
763
763
|
return lineWindow(await fsp.readFile(resolved, "utf8"), payload.offset, payload.limit);
|
|
764
764
|
}
|
|
765
765
|
|
|
766
766
|
async function write_file(payload) {
|
|
767
767
|
assertWritable("write_file");
|
|
768
|
-
const resolved = resolvePath(payload.
|
|
768
|
+
const resolved = resolvePath(payload.path);
|
|
769
769
|
await fsp.mkdir(path.dirname(resolved), { recursive: true });
|
|
770
770
|
const existed = fs.existsSync(resolved);
|
|
771
771
|
await fsp.writeFile(resolved, payload.content, "utf8");
|
|
@@ -774,14 +774,14 @@ async function write_file(payload) {
|
|
|
774
774
|
|
|
775
775
|
async function edit_file(payload) {
|
|
776
776
|
assertWritable("edit_file");
|
|
777
|
-
const resolved = resolvePath(payload.
|
|
777
|
+
const resolved = resolvePath(payload.path);
|
|
778
778
|
const edits = payload.edits || [{ old_text: payload.old_text, new_text: payload.new_text }];
|
|
779
779
|
let content = await fsp.readFile(resolved, "utf8");
|
|
780
780
|
for (const edit of edits) {
|
|
781
781
|
const oldText = edit.old_text || "";
|
|
782
782
|
const newText = edit.new_text || "";
|
|
783
783
|
if (oldText === "" || content.split(oldText).length - 1 !== 1) {
|
|
784
|
-
throw new Error("Could not locate old_text exactly once in " + payload.
|
|
784
|
+
throw new Error("Could not locate old_text exactly once in " + payload.path);
|
|
785
785
|
}
|
|
786
786
|
content = content.replace(oldText, newText);
|
|
787
787
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CloudflareProgrammaticToolCalling.cjs","names":["resolveCloudflareSandbox","getCloudflareWorkspaceRoot","ProgrammaticToolCallingSchema","BashProgrammaticToolCallingSchema","filterBashToolsByUsage","filterToolsByUsage","normalizeToPythonIdentifier","normalizeToBashIdentifier","validateCloudflareBashCommand","executeCloudflareCode","formatCompletedResponse","ProgrammaticToolCallingName","ProgrammaticToolCallingDescription","BashProgrammaticToolCallingDescription"],"sources":["../../../../src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts"],"sourcesContent":["import { tool } from '@langchain/core/tools';\nimport type { DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\n\n/* eslint-disable no-useless-escape -- generated sandbox helper source needs escapes for emitted JS/Python string literals. */\nimport {\n formatCompletedResponse,\n normalizeToPythonIdentifier,\n ProgrammaticToolCallingDescription,\n ProgrammaticToolCallingName,\n ProgrammaticToolCallingSchema,\n filterToolsByUsage,\n} from '@/tools/ProgrammaticToolCalling';\nimport {\n BashProgrammaticToolCallingDescription,\n BashProgrammaticToolCallingSchema,\n filterBashToolsByUsage,\n normalizeToBashIdentifier,\n} from '@/tools/BashProgrammaticToolCalling';\nimport { Constants } from '@/common';\nimport {\n executeCloudflareCode,\n getCloudflareWorkspaceRoot,\n resolveCloudflareSandbox,\n validateCloudflareBashCommand,\n} from './CloudflareSandboxExecutionEngine';\n\ntype ProgrammaticParams = {\n code: string;\n timeout?: number;\n lang?: string;\n runtime?: string;\n language?: string;\n};\n\nconst DEFAULT_TIMEOUT = 60000;\nconst MIN_TIMEOUT = 1000;\nconst MAX_TIMEOUT = 300000;\nconst DEFAULT_MAX_OUTPUT_CHARS = 200000;\n\ntype TimeoutSchema = {\n type: 'integer';\n minimum: number;\n maximum: number;\n default: number;\n description: string;\n};\n\ntype CloudflareProgrammaticToolCallingJsonSchema = {\n type: 'object';\n properties: typeof ProgrammaticToolCallingSchema.properties & {\n timeout: TimeoutSchema;\n lang: {\n type: 'string';\n enum: readonly ['py', 'python', 'bash', 'sh'];\n default: 'bash';\n description: string;\n };\n };\n required: readonly ['code'];\n};\n\ntype CloudflareBashProgrammaticToolCallingJsonSchema = {\n type: 'object';\n properties: typeof BashProgrammaticToolCallingSchema.properties & {\n timeout: TimeoutSchema;\n };\n required: readonly ['code'];\n};\n\nconst NATIVE_TOOL_NAMES = new Set<string>([\n Constants.READ_FILE,\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n Constants.BASH_TOOL,\n Constants.EXECUTE_CODE,\n]);\n\nfunction normalizeTimeout(timeoutMs: number | undefined): number {\n if (timeoutMs == null || !Number.isFinite(timeoutMs)) {\n return DEFAULT_TIMEOUT;\n }\n return Math.max(MIN_TIMEOUT, Math.floor(timeoutMs));\n}\n\nfunction formatTimeout(timeoutMs: number): string {\n return timeoutMs % 1000 === 0\n ? `${timeoutMs / 1000} seconds`\n : `${timeoutMs} milliseconds`;\n}\n\nfunction createTimeoutSchema(timeoutMs?: number): TimeoutSchema {\n const defaultTimeout = normalizeTimeout(timeoutMs);\n const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);\n return {\n type: 'integer',\n minimum: MIN_TIMEOUT,\n maximum: maxTimeout,\n default: defaultTimeout,\n description:\n 'Maximum Cloudflare Sandbox execution time in milliseconds. ' +\n `Default: ${formatTimeout(defaultTimeout)}. Max: ${formatTimeout(maxTimeout)}.`,\n };\n}\n\nfunction clampExecutionTimeout(\n requestedTimeoutMs: number | undefined,\n configuredTimeoutMs: number | undefined\n): number {\n const defaultTimeout = normalizeTimeout(configuredTimeoutMs);\n const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);\n if (requestedTimeoutMs == null || !Number.isFinite(requestedTimeoutMs)) {\n return defaultTimeout;\n }\n return Math.min(\n Math.max(MIN_TIMEOUT, Math.floor(requestedTimeoutMs)),\n maxTimeout\n );\n}\n\nfunction quoteShell(value: string): string {\n if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {\n return value;\n }\n const escapedQuote = String.raw`'\\''`;\n return `'${value.replace(/'/g, escapedQuote)}'`;\n}\n\nfunction truncateOutput(\n value: string,\n maxChars = DEFAULT_MAX_OUTPUT_CHARS\n): string {\n if (value.length <= maxChars) {\n return value;\n }\n const head = Math.floor(maxChars * 0.6);\n const tail = maxChars - head;\n const omitted = value.length - maxChars;\n return `${value.slice(0, head)}\\n\\n[... ${omitted} characters truncated ...]\\n\\n${value.slice(\n value.length - tail\n )}`;\n}\n\nfunction withInSandboxTimeout(command: string, timeoutMs: number): string {\n const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));\n return `timeout -k 2s ${timeoutSeconds}s ${command}`;\n}\n\nfunction outerTimeoutMs(timeoutMs: number): number {\n return timeoutMs + 5000;\n}\n\nfunction isInSandboxTimeoutExit(exitCode: number | null): boolean {\n return exitCode === 124 || exitCode === 137;\n}\n\nasync function executeGeneratedCloudflareBash(\n command: string,\n config: t.CloudflareSandboxExecutionConfig\n): ReturnType<typeof executeCloudflareCode> {\n const sandbox = await resolveCloudflareSandbox(config);\n const workspaceRoot = getCloudflareWorkspaceRoot(config);\n const shell = config.shell ?? 'bash';\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;\n const result = await sandbox.exec(\n withInSandboxTimeout(`${shell} -lc ${quoteShell(command)}`, timeoutMs),\n {\n cwd: workspaceRoot,\n env: config.env,\n timeout: outerTimeoutMs(timeoutMs),\n }\n );\n const maxOutputChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;\n return {\n stdout: truncateOutput(result.stdout, maxOutputChars),\n stderr: truncateOutput(result.stderr, maxOutputChars),\n exitCode: result.exitCode,\n timedOut: isInSandboxTimeoutExit(result.exitCode),\n };\n}\n\nfunction createCloudflareProgrammaticToolCallingSchema(\n config: t.CloudflareSandboxExecutionConfig\n): CloudflareProgrammaticToolCallingJsonSchema {\n return {\n ...ProgrammaticToolCallingSchema,\n properties: {\n ...ProgrammaticToolCallingSchema.properties,\n timeout: createTimeoutSchema(config.timeoutMs),\n lang: {\n type: 'string',\n enum: ['py', 'python', 'bash', 'sh'],\n default: 'bash',\n description:\n 'Cloudflare Sandbox runtime for orchestration code. Defaults to bash; use py/python for Python orchestration.',\n },\n },\n } as const;\n}\n\nfunction createCloudflareBashProgrammaticToolCallingSchema(\n config: t.CloudflareSandboxExecutionConfig\n): CloudflareBashProgrammaticToolCallingJsonSchema {\n return {\n ...BashProgrammaticToolCallingSchema,\n properties: {\n ...BashProgrammaticToolCallingSchema.properties,\n timeout: createTimeoutSchema(config.timeoutMs),\n },\n } as const;\n}\n\nfunction resolveRuntime(params: ProgrammaticParams): 'python' | 'bash' {\n const raw = params.lang ?? params.runtime ?? params.language ?? 'bash';\n return raw === 'py' || raw === 'python' ? 'python' : 'bash';\n}\n\nfunction filterNativeTools(\n toolDefs: t.LCTool[],\n code: string,\n runtime: 'python' | 'bash'\n): t.LCTool[] {\n const nativeDefs = toolDefs.filter((def) => NATIVE_TOOL_NAMES.has(def.name));\n const filter =\n runtime === 'bash' ? filterBashToolsByUsage : filterToolsByUsage;\n return filter(nativeDefs, code);\n}\n\nfunction indent(code: string, spaces = 4): string {\n const prefix = ' '.repeat(spaces);\n return code\n .split('\\n')\n .map((line) => (line === '' ? line : prefix + line))\n .join('\\n');\n}\n\nfunction pythonBoolean(value: boolean | undefined): 'True' | 'False' {\n return value === true ? 'True' : 'False';\n}\n\nfunction createPythonNativeToolSource(\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n return `\nimport asyncio, fnmatch, glob, json, os, pathlib, re, shlex, shutil, subprocess, sys, tempfile\n\nWORKSPACE = ${JSON.stringify(workspaceRoot)}\nSHELL = ${JSON.stringify(config.shell ?? 'bash')}\nREAD_ONLY = ${pythonBoolean(config.readOnly)}\nALLOW_DANGEROUS_COMMANDS = ${pythonBoolean(config.allowDangerousCommands)}\nDESTRUCTIVE_TARGET = r\"(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:/?\\\\.?\\\\*|/)?\"\nDANGEROUS_COMMAND_PATTERNS = [\n re.compile(r\"\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+|-[^\\\\s]*[r][^\\\\s]*\\\\s+-[^\\\\s]*[f][^\\\\s]*\\\\s+)(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"\\\\s*(?:$|[;&|])\"),\n re.compile(r\"\\\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\\\b\"),\n re.compile(r\"\\\\bdd\\\\s+[^;&|]*\\\\bof=/dev/\"),\n re.compile(r\"\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"(?:$|\\\\s|[;&|])\"),\n re.compile(r\"\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"(?:$|\\\\s|[;&|])\"),\n re.compile(r\":\\\\s*\\\\(\\\\s*\\\\)\\\\s*\\\\{\\\\s*:\\\\s*\\\\|\\\\s*:\\\\s*&\\\\s*\\\\}\\\\s*;\\\\s*:\"),\n]\nQUOTED_DESTRUCTIVE_PATTERNS = [\n re.compile(r\"\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+){1,3}(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n re.compile(r\"\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n re.compile(r\"\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n]\nNESTED_SHELL_PREFIX = r\"(?:(?:ba|z|da|k)?sh|eval)\\\\s+(?:-l?c\\\\s+)?\"\nNESTED_SHELL_DESTRUCTIVE_PATTERNS = [\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\brm\\\\s+-[^\\\\s\\\\\"']*[rf][^\\\\s\\\\\"']*\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n]\nMUTATING_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*\")\nPROTECTED_TARGET_ARG_RE = re.compile(r\"^(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:/?\\\\.?\\\\*|/)?$\")\nDESTRUCTIVE_OP_IN_COMMAND_RE = re.compile(r\"\\\\b(?:rm\\\\s+-[^\\\\s]*[rf]|chmod\\\\s+-R|chown\\\\s+-R)\\\\b\")\n\ndef _is_within_workspace(file_path):\n resolved = os.path.abspath(file_path)\n root = os.path.abspath(WORKSPACE)\n return os.path.commonpath([root, resolved]) == root\n\ndef _resolve(file_path=\".\"):\n raw = file_path or \".\"\n candidate = raw if os.path.isabs(raw) else os.path.join(WORKSPACE, raw)\n resolved = os.path.abspath(candidate)\n if not _is_within_workspace(resolved):\n raise ValueError(f\"Path is outside the Cloudflare sandbox workspace: {file_path}\")\n return resolved\n\ndef _assert_writable(tool_name):\n if READ_ONLY:\n raise PermissionError(f\"{tool_name} is blocked in read-only Cloudflare sandbox mode.\")\n\ndef _strip_quoted_content(command):\n output = []\n quote = None\n escaped = False\n index = 0\n while index < len(command):\n char = command[index]\n if escaped:\n escaped = False\n output.append(\" \")\n index += 1\n continue\n if char == \"\\\\\\\\\":\n escaped = True\n output.append(\" \")\n index += 1\n continue\n if quote is not None:\n if char == quote:\n quote = None\n output.append(\" \")\n index += 1\n continue\n if char in (\"'\", '\"', \"\\`\"):\n quote = char\n output.append(\" \")\n index += 1\n continue\n if char == \"#\":\n while index < len(command) and command[index] != \"\\\\n\":\n output.append(\" \")\n index += 1\n output.append(\"\\\\n\")\n index += 1\n continue\n output.append(char)\n index += 1\n return \"\".join(output)\n\ndef _validate_bash_command(command, args=None):\n errors = []\n normalized = _strip_quoted_content(command)\n if command.strip() == \"\":\n errors.append(\"Command is empty.\")\n if \"\\\\0\" in command:\n errors.append(\"Command contains a NUL byte.\")\n if not ALLOW_DANGEROUS_COMMANDS:\n if any(pattern.search(normalized) for pattern in DANGEROUS_COMMAND_PATTERNS):\n errors.append(\"Command matches a destructive command pattern.\")\n elif any(pattern.search(command) for pattern in QUOTED_DESTRUCTIVE_PATTERNS):\n errors.append(\"Command matches a destructive command pattern (quoted target).\")\n elif any(pattern.search(command) for pattern in NESTED_SHELL_DESTRUCTIVE_PATTERNS):\n errors.append(\"Command matches a destructive command pattern (nested shell payload).\")\n elif args and DESTRUCTIVE_OP_IN_COMMAND_RE.search(command):\n offending = next((str(arg) for arg in args if PROTECTED_TARGET_ARG_RE.search(str(arg))), None)\n if offending is not None:\n errors.append(f\"Command matches a destructive command pattern (protected target \\\\\"{offending}\\\\\" passed via positional arg).\")\n if READ_ONLY and MUTATING_COMMAND_PATTERN.search(normalized):\n errors.append(\"Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.\")\n if errors:\n raise ValueError(\"\\\\n\".join(errors))\n\ndef _line_window(content, offset=None, limit=None):\n start = max((offset or 1) - 1, 0)\n lines = content.split(\"\\\\n\")\n selected = lines[start:] if not limit or limit <= 0 else lines[start:start + limit]\n return \"\\\\n\".join(f\"{start + idx + 1:6d}\\\\t{line}\" for idx, line in enumerate(selected))\n\ndef _run(command, timeout=None, args=None):\n _validate_bash_command(command, args=args)\n completed = subprocess.run(\n [SHELL, \"-lc\", command, \"--\"] + [str(arg) for arg in (args or [])],\n cwd=WORKSPACE,\n capture_output=True,\n text=True,\n timeout=(timeout / 1000 if timeout else None),\n )\n return {\n \"stdout\": completed.stdout,\n \"stderr\": completed.stderr,\n \"exit_code\": completed.returncode,\n }\n\ndef _format_run(result):\n text = \"\"\n if result.get(\"stdout\"):\n text += f\"stdout:\\\\n{result['stdout']}\\\\n\"\n else:\n text += \"stdout: Empty. Ensure you're writing output explicitly.\\\\n\"\n if result.get(\"stderr\"):\n text += f\"stderr:\\\\n{result['stderr']}\\\\n\"\n if result.get(\"exit_code\") not in (None, 0):\n text += f\"exit_code: {result['exit_code']}\\\\n\"\n text += f\"working_directory: {WORKSPACE}\"\n return text.strip()\n\ndef _detect_compile_command():\n if os.path.exists(os.path.join(WORKSPACE, \"tsconfig.json\")):\n return \"typescript\", \"npx --no-install tsc --noEmit\", \"tsconfig.json present\"\n package_json = os.path.join(WORKSPACE, \"package.json\")\n if os.path.exists(package_json):\n try:\n if '\"typescript\"' in open(package_json, encoding=\"utf-8\").read():\n return \"typescript\", \"npx --no-install tsc --noEmit\", \"package.json declares typescript\"\n except Exception:\n pass\n if os.path.exists(os.path.join(WORKSPACE, \"Cargo.toml\")):\n return \"rust\", \"cargo check --message-format=short\", \"Cargo.toml present\"\n if os.path.exists(os.path.join(WORKSPACE, \"go.mod\")):\n return \"go\", \"go vet ./...\", \"go.mod present\"\n if any(os.path.exists(os.path.join(WORKSPACE, name)) for name in [\"pyproject.toml\", \"setup.py\", \"setup.cfg\"]):\n return \"python-compile\", \"python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')\", \"Python project\"\n return \"unknown\", \"\", \"no recognised project marker\"\n\nasync def bash_tool(command, args=None):\n return _format_run(_run(command, args=args))\n\nasync def execute_code(lang, code, args=None):\n args = args or []\n temp_dir = tempfile.mkdtemp(prefix=\"lc-ptc-\", dir=WORKSPACE)\n try:\n def q(value):\n import shlex\n return shlex.quote(str(value))\n arg_text = \" \".join(q(arg) for arg in args)\n if lang in (\"py\", \"python\"):\n file_path = os.path.join(temp_dir, \"main.py\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"python3 {q(file_path)} {arg_text}\"))\n if lang in (\"js\", \"javascript\"):\n file_path = os.path.join(temp_dir, \"main.js\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"node {q(file_path)} {arg_text}\"))\n if lang in (\"ts\", \"typescript\"):\n file_path = os.path.join(temp_dir, \"main.ts\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"npx --no-install tsx {q(file_path)} {arg_text}\"))\n if lang == \"php\":\n file_path = os.path.join(temp_dir, \"main.php\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"php {q(file_path)} {arg_text}\"))\n if lang == \"go\":\n file_path = os.path.join(temp_dir, \"main.go\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"go run {q(file_path)} {arg_text}\"))\n if lang == \"rs\":\n file_path = os.path.join(temp_dir, \"main.rs\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-rs\")\n return _format_run(_run(f\"rustc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"c\":\n file_path = os.path.join(temp_dir, \"main.c\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-c\")\n return _format_run(_run(f\"cc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"cpp\":\n file_path = os.path.join(temp_dir, \"main.cpp\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-cpp\")\n return _format_run(_run(f\"c++ {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"java\":\n file_path = os.path.join(temp_dir, \"Main.java\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"javac {q(file_path)} && java -cp {q(temp_dir)} Main {arg_text}\"))\n if lang == \"r\":\n file_path = os.path.join(temp_dir, \"main.R\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"Rscript {q(file_path)} {arg_text}\"))\n if lang == \"d\":\n file_path = os.path.join(temp_dir, \"main.d\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-d\")\n return _format_run(_run(f\"dmd {q(file_path)} -of={q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"f90\":\n file_path = os.path.join(temp_dir, \"main.f90\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-f90\")\n return _format_run(_run(f\"gfortran {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang in (\"bash\", \"sh\"):\n return _format_run(_run(code, args=args))\n raise ValueError(f\"Unsupported Cloudflare sandbox runtime: {lang}\")\n finally:\n shutil.rmtree(temp_dir, ignore_errors=True)\n\nasync def read_file(file_path, offset=None, limit=None):\n resolved = _resolve(file_path)\n with open(resolved, encoding=\"utf-8\") as handle:\n return _line_window(handle.read(), offset, limit)\n\nasync def write_file(file_path, content):\n _assert_writable(\"write_file\")\n resolved = _resolve(file_path)\n os.makedirs(os.path.dirname(resolved), exist_ok=True)\n existed = os.path.exists(resolved)\n with open(resolved, \"w\", encoding=\"utf-8\") as handle:\n handle.write(content)\n return f\"{'Overwrote' if existed else 'Created'} {resolved} ({len(content)} chars).\"\n\nasync def edit_file(file_path, old_text=None, new_text=None, edits=None):\n _assert_writable(\"edit_file\")\n resolved = _resolve(file_path)\n edits = edits or [{\"old_text\": old_text, \"new_text\": new_text}]\n content = open(resolved, encoding=\"utf-8\").read()\n for edit in edits:\n old = edit.get(\"old_text\") or \"\"\n new = edit.get(\"new_text\") or \"\"\n if content.count(old) != 1:\n raise ValueError(f\"Could not locate old_text exactly once in {file_path}\")\n content = content.replace(old, new, 1)\n open(resolved, \"w\", encoding=\"utf-8\").write(content)\n return f\"Applied {len(edits)} edit(s) to {resolved}.\"\n\nasync def list_directory(path=\".\"):\n resolved = _resolve(path)\n entries = []\n for name in sorted(os.listdir(resolved)):\n full = os.path.join(resolved, name)\n entries.append((\"dir \" if os.path.isdir(full) else \"file\") + \"\\\\t\" + name)\n return \"\\\\n\".join(entries) or \"Directory is empty.\"\n\nasync def grep_search(pattern, path=\".\", glob=None, max_results=200):\n root = _resolve(path)\n regex = re.compile(pattern)\n out = []\n for current, dirs, files in os.walk(root):\n dirs[:] = [d for d in dirs if d not in {\".git\", \"node_modules\", \".venv\", \"dist\", \"build\"}]\n for name in files:\n rel = os.path.relpath(os.path.join(current, name), root)\n if glob and not fnmatch.fnmatch(rel, glob):\n continue\n try:\n for line_no, line in enumerate(open(os.path.join(current, name), encoding=\"utf-8\", errors=\"ignore\"), 1):\n if regex.search(line):\n out.append(f\"{os.path.join(current, name)}:{line_no}:{line.rstrip()}\")\n if len(out) >= max_results:\n return \"\\\\n\".join(out)\n except Exception:\n pass\n return \"\\\\n\".join(out) if out else \"No matches found.\"\n\nasync def glob_search(pattern, path=\".\", max_results=200):\n root = _resolve(path)\n target = pattern if os.path.isabs(pattern) else os.path.join(root, pattern)\n matches = []\n for match in glob_module.glob(target, recursive=True):\n resolved = os.path.abspath(match)\n if _is_within_workspace(resolved):\n matches.append(resolved)\n if len(matches) >= max_results:\n break\n return \"\\\\n\".join(matches) if matches else \"No files found.\"\n\nasync def compile_check(command=None, timeout_ms=None):\n kind, detected, reason = _detect_compile_command()\n command = command or detected\n if not command:\n return f\"compile_check: {reason}. Pass an explicit command to override.\"\n result = _run(command, timeout_ms)\n status = \"PASSED\" if result[\"exit_code\"] == 0 else \"FAILED\"\n return f\"compile_check ({kind}) {status} via {command}\\\\n\\\\nstdout:\\\\n{result['stdout']}\\\\nstderr:\\\\n{result['stderr']}\\\\nworking_directory: {WORKSPACE}\\\\nreason: {reason}\"\n\n# Avoid shadowing the glob_search function argument named \"glob\".\nglob_module = glob\n`.trim();\n}\n\nfunction createNodeNativeToolSource(\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n return `\nconst fs = require(\"fs\");\nconst fsp = fs.promises;\nconst path = require(\"path\");\nconst cp = require(\"child_process\");\n\nconst WORKSPACE = ${JSON.stringify(workspaceRoot)};\nconst SHELL = ${JSON.stringify(config.shell ?? 'bash')};\nconst READ_ONLY = ${JSON.stringify(config.readOnly === true)};\nconst ALLOW_DANGEROUS_COMMANDS = ${JSON.stringify(config.allowDangerousCommands === true)};\nconst DESTRUCTIVE_TARGET = \"(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)(?:\\\\\\\\/?\\\\\\\\.?\\\\\\\\*|\\\\\\\\/)?\";\nconst DANGEROUS_COMMAND_PATTERNS = [\n new RegExp(\"\\\\\\\\brm\\\\\\\\s+(?:-[^\\\\\\\\s]*[rf][^\\\\\\\\s]*\\\\\\\\s+|-[^\\\\\\\\s]*[r][^\\\\\\\\s]*\\\\\\\\s+-[^\\\\\\\\s]*[f][^\\\\\\\\s]*\\\\\\\\s+)(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"\\\\\\\\s*(?:$|[;&|])\"),\n /\\\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\\\b/,\n /\\\\bdd\\\\s+[^;&|]*\\\\bof=\\\\/dev\\\\//,\n new RegExp(\"\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"(?:$|\\\\\\\\s|[;&|])\"),\n new RegExp(\"\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"(?:$|\\\\\\\\s|[;&|])\"),\n /:\\\\s*\\\\(\\\\s*\\\\)\\\\s*\\\\{\\\\s*:\\\\s*\\\\|\\\\s*:\\\\s*&\\\\s*\\\\}\\\\s*;\\\\s*:/,\n];\nconst QUOTED_DESTRUCTIVE_PATTERNS = [\n new RegExp(\"\\\\\\\\brm\\\\\\\\s+(?:-[^\\\\\\\\s]*[rf][^\\\\\\\\s]*\\\\\\\\s+){1,3}(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n new RegExp(\"\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n new RegExp(\"\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n];\nconst NESTED_SHELL_PREFIX = \"(?:(?:ba|z|da|k)?sh|eval)\\\\\\\\s+(?:-l?c\\\\\\\\s+)?\";\nconst NESTED_SHELL_DESTRUCTIVE_PATTERNS = [\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\brm\\\\\\\\s+-[^\\\\\\\\s\\\\\\\"']*[rf][^\\\\\\\\s\\\\\\\"']*\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n];\nconst 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*/;\nconst PROTECTED_TARGET_ARG_RE = /^(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:\\\\/?\\\\.?\\\\*|\\\\/)?$/;\nconst DESTRUCTIVE_OP_IN_COMMAND_RE = /\\\\b(?:rm\\\\s+-[^\\\\s]*[rf]|chmod\\\\s+-R|chown\\\\s+-R)\\\\b/;\n\nfunction resolvePath(filePath) {\n const raw = filePath || \".\";\n const candidate = path.isAbsolute(raw) ? raw : path.join(WORKSPACE, raw);\n const resolved = path.resolve(candidate);\n const root = path.resolve(WORKSPACE);\n const relative = path.relative(root, resolved);\n if (relative && (relative.startsWith(\"..\") || path.isAbsolute(relative))) {\n throw new Error(\"Path is outside the Cloudflare sandbox workspace: \" + filePath);\n }\n return resolved;\n}\n\nfunction assertWritable(toolName) {\n if (READ_ONLY) {\n throw new Error(toolName + \" is blocked in read-only Cloudflare sandbox mode.\");\n }\n}\n\nfunction stripQuotedContent(command) {\n let output = \"\";\n let quote;\n let escaped = false;\n for (let index = 0; index < command.length; index++) {\n const char = command[index];\n if (escaped) {\n escaped = false;\n output += \" \";\n continue;\n }\n if (char === \"\\\\\\\\\") {\n escaped = true;\n output += \" \";\n continue;\n }\n if (quote != null) {\n if (char === quote) quote = undefined;\n output += \" \";\n continue;\n }\n if (char === \"\\\\\\\"\" || char === \"'\" || char === \"\\`\") {\n quote = char;\n output += \" \";\n continue;\n }\n if (char === \"#\") {\n while (index < command.length && command[index] !== \"\\\\n\") {\n output += \" \";\n index += 1;\n }\n output += \"\\\\n\";\n continue;\n }\n output += char;\n }\n return output;\n}\n\nfunction validateBashCommand(command, args) {\n const errors = [];\n const normalized = stripQuotedContent(command);\n if (command.trim() === \"\") {\n errors.push(\"Command is empty.\");\n }\n if (command.includes(\"\\\\0\")) {\n errors.push(\"Command contains a NUL byte.\");\n }\n if (!ALLOW_DANGEROUS_COMMANDS) {\n if (DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {\n errors.push(\"Command matches a destructive command pattern.\");\n } else if (QUOTED_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {\n errors.push(\"Command matches a destructive command pattern (quoted target).\");\n } else if (NESTED_SHELL_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {\n errors.push(\"Command matches a destructive command pattern (nested shell payload).\");\n } else if ((args || []).length > 0 && DESTRUCTIVE_OP_IN_COMMAND_RE.test(command)) {\n const offending = (args || []).map(String).find((arg) => PROTECTED_TARGET_ARG_RE.test(arg));\n if (offending !== undefined) {\n errors.push(\"Command matches a destructive command pattern (protected target \\\\\"\" + offending + \"\\\\\" passed via positional arg).\");\n }\n }\n }\n if (READ_ONLY && MUTATING_COMMAND_PATTERN.test(normalized)) {\n errors.push(\"Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.\");\n }\n if (errors.length > 0) {\n throw new Error(errors.join(\"\\\\n\"));\n }\n}\n\nfunction lineWindow(content, offset, limit) {\n const start = Math.max((offset || 1) - 1, 0);\n const lines = content.split(\"\\\\n\");\n const selected = !limit || limit <= 0 ? lines.slice(start) : lines.slice(start, start + limit);\n return selected.map((line, index) => String(start + index + 1).padStart(6, \" \") + \"\\\\t\" + line).join(\"\\\\n\");\n}\n\nfunction quote(value) {\n const text = String(value);\n if (text === \"\") return \"''\";\n if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(text)) return text;\n return \"'\" + text.replace(/'/g, \"'\\\\\\\\''\") + \"'\";\n}\n\nfunction run(command, timeoutMs, args) {\n validateBashCommand(command, args);\n return new Promise((resolve) => {\n const child = cp.spawn(SHELL, [\"-lc\", command, \"--\", ...((args || []).map(String))], {\n cwd: WORKSPACE,\n env: process.env,\n });\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n const timer = timeoutMs\n ? setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n }, timeoutMs)\n : null;\n\n child.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n child.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n child.on(\"error\", (error) => {\n if (timer) clearTimeout(timer);\n resolve({ stdout, stderr: stderr + error.message, exit_code: 1, timed_out: timedOut });\n });\n child.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n resolve({ stdout, stderr, exit_code: timedOut ? null : code, timed_out: timedOut });\n });\n });\n}\n\nfunction formatRun(result) {\n let text = \"\";\n if (result.stdout) {\n text += \"stdout:\\\\n\" + result.stdout + \"\\\\n\";\n } else {\n text += \"stdout: Empty. Ensure you're writing output explicitly.\\\\n\";\n }\n if (result.stderr) {\n text += \"stderr:\\\\n\" + result.stderr + \"\\\\n\";\n }\n if (result.timed_out) {\n text += \"timed_out: true\\\\n\";\n }\n if (result.exit_code !== null && result.exit_code !== undefined && result.exit_code !== 0) {\n text += \"exit_code: \" + result.exit_code + \"\\\\n\";\n }\n text += \"working_directory: \" + WORKSPACE;\n return text.trim();\n}\n\nasync function detectCompileCommand() {\n async function exists(name) {\n try {\n await fsp.access(path.join(WORKSPACE, name));\n return true;\n } catch {\n return false;\n }\n }\n if (await exists(\"tsconfig.json\")) {\n return [\"typescript\", \"npx --no-install tsc --noEmit\", \"tsconfig.json present\"];\n }\n if (await exists(\"package.json\")) {\n try {\n if ((await fsp.readFile(path.join(WORKSPACE, \"package.json\"), \"utf8\")).includes('\"typescript\"')) {\n return [\"typescript\", \"npx --no-install tsc --noEmit\", \"package.json declares typescript\"];\n }\n } catch {}\n }\n if (await exists(\"Cargo.toml\")) return [\"rust\", \"cargo check --message-format=short\", \"Cargo.toml present\"];\n if (await exists(\"go.mod\")) return [\"go\", \"go vet ./...\", \"go.mod present\"];\n if (await exists(\"pyproject.toml\") || await exists(\"setup.py\") || await exists(\"setup.cfg\")) {\n return [\"python-compile\", \"python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')\", \"Python project\"];\n }\n return [\"unknown\", \"\", \"no recognised project marker\"];\n}\n\nfunction globToRegExp(pattern) {\n const escaped = pattern.replace(/[|\\\\\\\\{}()[\\\\]^$+*?.]/g, \"\\\\\\\\$&\");\n return new RegExp(\"^\" + escaped.replace(/\\\\\\\\\\\\*\\\\\\\\\\\\*/g, \".*\").replace(/\\\\\\\\\\\\*/g, \"[^/]*\") + \"$\");\n}\n\nfunction globMatch(relativePath, pattern) {\n const matcher = globToRegExp(pattern);\n return matcher.test(relativePath) || matcher.test(path.basename(relativePath));\n}\n\nasync function walkFiles(root, visit) {\n const entries = await fsp.readdir(root, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(root, entry.name);\n if (entry.isDirectory()) {\n if ([\".git\", \"node_modules\", \".venv\", \"dist\", \"build\"].includes(entry.name)) continue;\n await walkFiles(full, visit);\n } else if (entry.isFile()) {\n await visit(full);\n }\n }\n}\n\nasync function bash_tool(payload) {\n return formatRun(await run(payload.command, undefined, payload.args));\n}\n\nasync function execute_code(payload) {\n const lang = payload.lang;\n const code = payload.code;\n const args = payload.args || [];\n const tempDir = await fsp.mkdtemp(path.join(WORKSPACE, \"lc-ptc-\"));\n try {\n const argText = args.map(quote).join(\" \");\n async function writeAndRun(fileName, command) {\n const filePath = path.join(tempDir, fileName);\n await fsp.writeFile(filePath, code, \"utf8\");\n return formatRun(await run(command(filePath, argText), undefined, []));\n }\n if (lang === \"py\" || lang === \"python\") {\n return writeAndRun(\"main.py\", (filePath, argText) => \"python3 \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"js\" || lang === \"javascript\") {\n return writeAndRun(\"main.js\", (filePath, argText) => \"node \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"ts\" || lang === \"typescript\") {\n return writeAndRun(\"main.ts\", (filePath, argText) => \"npx --no-install tsx \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"php\") {\n return writeAndRun(\"main.php\", (filePath, argText) => \"php \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"go\") {\n return writeAndRun(\"main.go\", (filePath, argText) => \"go run \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"rs\") {\n return writeAndRun(\"main.rs\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-rs\");\n return \"rustc \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"c\") {\n return writeAndRun(\"main.c\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-c\");\n return \"cc \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"cpp\") {\n return writeAndRun(\"main.cpp\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-cpp\");\n return \"c++ \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"java\") {\n return writeAndRun(\"Main.java\", (filePath, argText) => \"javac \" + quote(filePath) + \" && java -cp \" + quote(tempDir) + \" Main \" + argText);\n }\n if (lang === \"r\") {\n return writeAndRun(\"main.R\", (filePath, argText) => \"Rscript \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"d\") {\n return writeAndRun(\"main.d\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-d\");\n return \"dmd \" + quote(filePath) + \" -of=\" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"f90\") {\n return writeAndRun(\"main.f90\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-f90\");\n return \"gfortran \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"bash\" || lang === \"sh\") {\n return formatRun(await run(code, undefined, args));\n }\n throw new Error(\"Unsupported Cloudflare sandbox runtime: \" + lang);\n } finally {\n await fsp.rm(tempDir, { recursive: true, force: true });\n }\n}\n\nasync function read_file(payload) {\n const resolved = resolvePath(payload.file_path);\n return lineWindow(await fsp.readFile(resolved, \"utf8\"), payload.offset, payload.limit);\n}\n\nasync function write_file(payload) {\n assertWritable(\"write_file\");\n const resolved = resolvePath(payload.file_path);\n await fsp.mkdir(path.dirname(resolved), { recursive: true });\n const existed = fs.existsSync(resolved);\n await fsp.writeFile(resolved, payload.content, \"utf8\");\n return (existed ? \"Overwrote \" : \"Created \") + resolved + \" (\" + payload.content.length + \" chars).\";\n}\n\nasync function edit_file(payload) {\n assertWritable(\"edit_file\");\n const resolved = resolvePath(payload.file_path);\n const edits = payload.edits || [{ old_text: payload.old_text, new_text: payload.new_text }];\n let content = await fsp.readFile(resolved, \"utf8\");\n for (const edit of edits) {\n const oldText = edit.old_text || \"\";\n const newText = edit.new_text || \"\";\n if (oldText === \"\" || content.split(oldText).length - 1 !== 1) {\n throw new Error(\"Could not locate old_text exactly once in \" + payload.file_path);\n }\n content = content.replace(oldText, newText);\n }\n await fsp.writeFile(resolved, content, \"utf8\");\n return \"Applied \" + edits.length + \" edit(s) to \" + resolved + \".\";\n}\n\nasync function list_directory(payload) {\n const resolved = resolvePath(payload.path || \".\");\n const entries = await fsp.readdir(resolved, { withFileTypes: true });\n const lines = entries\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((entry) => (entry.isDirectory() ? \"dir\" : \"file\") + \"\\\\t\" + entry.name);\n return lines.join(\"\\\\n\") || \"Directory is empty.\";\n}\n\nasync function grep_search(payload) {\n const root = resolvePath(payload.path || \".\");\n const regex = new RegExp(payload.pattern);\n const maxResults = payload.max_results || 200;\n const out = [];\n await walkFiles(root, async (filePath) => {\n if (out.length >= maxResults) return;\n const relative = path.relative(root, filePath);\n if (payload.glob && !globMatch(relative, payload.glob)) return;\n let text = \"\";\n try {\n text = await fsp.readFile(filePath, \"utf8\");\n } catch {\n return;\n }\n text.split(\"\\\\n\").forEach((line, index) => {\n if (out.length < maxResults && regex.test(line)) {\n out.push(filePath + \":\" + (index + 1) + \":\" + line);\n }\n });\n });\n return out.join(\"\\\\n\") || \"No matches found.\";\n}\n\nasync function glob_search(payload) {\n const root = resolvePath(payload.path || \".\");\n const maxResults = payload.max_results || 200;\n const out = [];\n await walkFiles(root, async (filePath) => {\n if (out.length >= maxResults) return;\n const relative = path.relative(root, filePath);\n if (globMatch(relative, payload.pattern)) out.push(filePath);\n });\n return out.join(\"\\\\n\") || \"No files found.\";\n}\n\nasync function compile_check(payload) {\n const [kind, detected, reason] = await detectCompileCommand();\n const command = payload.command || detected;\n if (!command) {\n return \"compile_check: \" + reason + \". Pass an explicit command to override.\";\n }\n const result = await run(command, payload.timeout_ms);\n const status = result.exit_code === 0 ? \"PASSED\" : \"FAILED\";\n return \"compile_check (\" + kind + \") \" + status + \" via \" + command + \"\\\\n\\\\nstdout:\\\\n\" + result.stdout + \"\\\\nstderr:\\\\n\" + result.stderr + \"\\\\nworking_directory: \" + WORKSPACE + \"\\\\nreason: \" + reason;\n}\n\nconst TOOLS = {\n bash_tool,\n execute_code,\n read_file,\n write_file,\n edit_file,\n list_directory,\n grep_search,\n glob_search,\n compile_check,\n};\n\nasync function main() {\n const name = process.argv[2];\n const payload = JSON.parse(process.argv[3] || \"{}\");\n if (!TOOLS[name]) throw new Error(\"Unknown tool: \" + name);\n const result = await TOOLS[name](payload);\n process.stdout.write(typeof result === \"string\" ? result : JSON.stringify(result));\n}\n\nmain().catch((error) => {\n console.error(error && error.stack ? error.stack : String(error));\n process.exit(1);\n});\n`.trim();\n}\n\nfunction createPythonProgram(\n userCode: string,\n toolDefs: t.LCTool[],\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n const aliases = toolDefs\n .map((def) => {\n const pythonName = normalizeToPythonIdentifier(def.name);\n return NATIVE_TOOL_NAMES.has(def.name) && pythonName !== def.name\n ? `${pythonName} = globals()[${JSON.stringify(def.name)}]`\n : '';\n })\n .filter(Boolean)\n .join('\\n');\n return `${createPythonNativeToolSource(config, workspaceRoot)}\n${aliases}\n\nasync def __lc_user_main__():\n${indent(userCode)}\n\nasyncio.run(__lc_user_main__())\n`;\n}\n\nfunction createBashProgram(\n userCode: string,\n toolDefs: t.LCTool[],\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n const helper = createNodeNativeToolSource(config, workspaceRoot);\n const functions = toolDefs\n .map((def) => {\n const bashName = normalizeToBashIdentifier(def.name);\n if (!NATIVE_TOOL_NAMES.has(def.name)) {\n return '';\n }\n return `${bashName}() { node \"$__LC_TOOL_HELPER\" ${JSON.stringify(def.name)} \"$1\"; }`;\n })\n .filter(Boolean)\n .join('\\n');\n return `\nset -euo pipefail\ncommand -v node >/dev/null 2>&1 || { echo \"Cloudflare programmatic tool calling requires node in the sandbox image.\" >&2; exit 127; }\n__LC_TOOL_HELPER=\"$(mktemp /tmp/lc-tools.XXXXXX.js)\"\ncat > \"$__LC_TOOL_HELPER\" <<'JS'\n${helper}\nJS\ntrap 'rm -f \"$__LC_TOOL_HELPER\"' EXIT\n${functions}\n${userCode}\n`.trim();\n}\n\nasync function runProgrammatic(args: {\n params: ProgrammaticParams;\n config?: { toolCall?: unknown };\n cloudflareConfig: t.CloudflareSandboxExecutionConfig;\n runtime: 'python' | 'bash';\n}): Promise<[string, t.ProgrammaticExecutionArtifact]> {\n const toolCall = (args.config?.toolCall ??\n {}) as Partial<t.ProgrammaticCache>;\n const toolDefs = toolCall.toolDefs ?? [];\n const effectiveTools = filterNativeTools(\n toolDefs,\n args.params.code,\n args.runtime\n );\n const timeoutMs = clampExecutionTimeout(\n args.params.timeout,\n args.cloudflareConfig.timeoutMs\n );\n const workspaceRoot = getCloudflareWorkspaceRoot(args.cloudflareConfig);\n let result: Awaited<ReturnType<typeof executeCloudflareCode>>;\n\n if (args.runtime === 'bash') {\n await validateCloudflareBashCommand(args.params.code, [], {\n ...args.cloudflareConfig,\n timeoutMs,\n });\n result = await executeGeneratedCloudflareBash(\n createBashProgram(\n args.params.code,\n effectiveTools,\n args.cloudflareConfig,\n workspaceRoot\n ),\n { ...args.cloudflareConfig, timeoutMs }\n );\n } else {\n result = await executeCloudflareCode(\n {\n lang: 'py',\n code: createPythonProgram(\n args.params.code,\n effectiveTools,\n args.cloudflareConfig,\n workspaceRoot\n ),\n },\n { ...args.cloudflareConfig, timeoutMs }\n );\n }\n\n if (result.exitCode !== 0 || result.timedOut) {\n throw new Error(\n result.stderr !== ''\n ? result.stderr\n : `Cloudflare ${args.runtime} programmatic execution exited with code ${result.exitCode ?? 'unknown'}`\n );\n }\n\n return formatCompletedResponse({\n status: 'completed',\n session_id: 'cloudflare-sandbox',\n stdout: result.stdout,\n stderr: result.stderr,\n files: [],\n });\n}\n\nexport function createCloudflareProgrammaticToolCallingTool(\n cloudflareConfig: t.CloudflareSandboxExecutionConfig\n): DynamicStructuredTool {\n return tool(\n async (rawParams, config) => {\n const params = rawParams as ProgrammaticParams;\n return runProgrammatic({\n params,\n config,\n cloudflareConfig,\n runtime: resolveRuntime(params),\n });\n },\n {\n name: ProgrammaticToolCallingName,\n 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.`,\n schema: createCloudflareProgrammaticToolCallingSchema(cloudflareConfig),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport function createCloudflareBashProgrammaticToolCallingTool(\n cloudflareConfig: t.CloudflareSandboxExecutionConfig\n): DynamicStructuredTool {\n return tool(\n async (rawParams, config) => {\n const params = rawParams as ProgrammaticParams;\n return runProgrammatic({\n params,\n config,\n cloudflareConfig,\n runtime: 'bash',\n });\n },\n {\n name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,\n 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.`,\n schema:\n createCloudflareBashProgrammaticToolCallingSchema(cloudflareConfig),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CloudflareProgrammaticToolCalling.cjs","names":["resolveCloudflareSandbox","getCloudflareWorkspaceRoot","ProgrammaticToolCallingSchema","BashProgrammaticToolCallingSchema","filterBashToolsByUsage","filterToolsByUsage","normalizeToPythonIdentifier","normalizeToBashIdentifier","validateCloudflareBashCommand","executeCloudflareCode","formatCompletedResponse","ProgrammaticToolCallingName","ProgrammaticToolCallingDescription","BashProgrammaticToolCallingDescription"],"sources":["../../../../src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts"],"sourcesContent":["import { tool } from '@langchain/core/tools';\nimport type { DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\n\n/* eslint-disable no-useless-escape -- generated sandbox helper source needs escapes for emitted JS/Python string literals. */\nimport {\n formatCompletedResponse,\n normalizeToPythonIdentifier,\n ProgrammaticToolCallingDescription,\n ProgrammaticToolCallingName,\n ProgrammaticToolCallingSchema,\n filterToolsByUsage,\n} from '@/tools/ProgrammaticToolCalling';\nimport {\n BashProgrammaticToolCallingDescription,\n BashProgrammaticToolCallingSchema,\n filterBashToolsByUsage,\n normalizeToBashIdentifier,\n} from '@/tools/BashProgrammaticToolCalling';\nimport { Constants } from '@/common';\nimport {\n executeCloudflareCode,\n getCloudflareWorkspaceRoot,\n resolveCloudflareSandbox,\n validateCloudflareBashCommand,\n} from './CloudflareSandboxExecutionEngine';\n\ntype ProgrammaticParams = {\n code: string;\n timeout?: number;\n lang?: string;\n runtime?: string;\n language?: string;\n};\n\nconst DEFAULT_TIMEOUT = 60000;\nconst MIN_TIMEOUT = 1000;\nconst MAX_TIMEOUT = 300000;\nconst DEFAULT_MAX_OUTPUT_CHARS = 200000;\n\ntype TimeoutSchema = {\n type: 'integer';\n minimum: number;\n maximum: number;\n default: number;\n description: string;\n};\n\ntype CloudflareProgrammaticToolCallingJsonSchema = {\n type: 'object';\n properties: typeof ProgrammaticToolCallingSchema.properties & {\n timeout: TimeoutSchema;\n lang: {\n type: 'string';\n enum: readonly ['py', 'python', 'bash', 'sh'];\n default: 'bash';\n description: string;\n };\n };\n required: readonly ['code'];\n};\n\ntype CloudflareBashProgrammaticToolCallingJsonSchema = {\n type: 'object';\n properties: typeof BashProgrammaticToolCallingSchema.properties & {\n timeout: TimeoutSchema;\n };\n required: readonly ['code'];\n};\n\nconst NATIVE_TOOL_NAMES = new Set<string>([\n Constants.READ_FILE,\n Constants.WRITE_FILE,\n Constants.EDIT_FILE,\n Constants.GREP_SEARCH,\n Constants.GLOB_SEARCH,\n Constants.LIST_DIRECTORY,\n Constants.COMPILE_CHECK,\n Constants.BASH_TOOL,\n Constants.EXECUTE_CODE,\n]);\n\nfunction normalizeTimeout(timeoutMs: number | undefined): number {\n if (timeoutMs == null || !Number.isFinite(timeoutMs)) {\n return DEFAULT_TIMEOUT;\n }\n return Math.max(MIN_TIMEOUT, Math.floor(timeoutMs));\n}\n\nfunction formatTimeout(timeoutMs: number): string {\n return timeoutMs % 1000 === 0\n ? `${timeoutMs / 1000} seconds`\n : `${timeoutMs} milliseconds`;\n}\n\nfunction createTimeoutSchema(timeoutMs?: number): TimeoutSchema {\n const defaultTimeout = normalizeTimeout(timeoutMs);\n const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);\n return {\n type: 'integer',\n minimum: MIN_TIMEOUT,\n maximum: maxTimeout,\n default: defaultTimeout,\n description:\n 'Maximum Cloudflare Sandbox execution time in milliseconds. ' +\n `Default: ${formatTimeout(defaultTimeout)}. Max: ${formatTimeout(maxTimeout)}.`,\n };\n}\n\nfunction clampExecutionTimeout(\n requestedTimeoutMs: number | undefined,\n configuredTimeoutMs: number | undefined\n): number {\n const defaultTimeout = normalizeTimeout(configuredTimeoutMs);\n const maxTimeout = Math.max(MAX_TIMEOUT, defaultTimeout);\n if (requestedTimeoutMs == null || !Number.isFinite(requestedTimeoutMs)) {\n return defaultTimeout;\n }\n return Math.min(\n Math.max(MIN_TIMEOUT, Math.floor(requestedTimeoutMs)),\n maxTimeout\n );\n}\n\nfunction quoteShell(value: string): string {\n if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) {\n return value;\n }\n const escapedQuote = String.raw`'\\''`;\n return `'${value.replace(/'/g, escapedQuote)}'`;\n}\n\nfunction truncateOutput(\n value: string,\n maxChars = DEFAULT_MAX_OUTPUT_CHARS\n): string {\n if (value.length <= maxChars) {\n return value;\n }\n const head = Math.floor(maxChars * 0.6);\n const tail = maxChars - head;\n const omitted = value.length - maxChars;\n return `${value.slice(0, head)}\\n\\n[... ${omitted} characters truncated ...]\\n\\n${value.slice(\n value.length - tail\n )}`;\n}\n\nfunction withInSandboxTimeout(command: string, timeoutMs: number): string {\n const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));\n return `timeout -k 2s ${timeoutSeconds}s ${command}`;\n}\n\nfunction outerTimeoutMs(timeoutMs: number): number {\n return timeoutMs + 5000;\n}\n\nfunction isInSandboxTimeoutExit(exitCode: number | null): boolean {\n return exitCode === 124 || exitCode === 137;\n}\n\nasync function executeGeneratedCloudflareBash(\n command: string,\n config: t.CloudflareSandboxExecutionConfig\n): ReturnType<typeof executeCloudflareCode> {\n const sandbox = await resolveCloudflareSandbox(config);\n const workspaceRoot = getCloudflareWorkspaceRoot(config);\n const shell = config.shell ?? 'bash';\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;\n const result = await sandbox.exec(\n withInSandboxTimeout(`${shell} -lc ${quoteShell(command)}`, timeoutMs),\n {\n cwd: workspaceRoot,\n env: config.env,\n timeout: outerTimeoutMs(timeoutMs),\n }\n );\n const maxOutputChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;\n return {\n stdout: truncateOutput(result.stdout, maxOutputChars),\n stderr: truncateOutput(result.stderr, maxOutputChars),\n exitCode: result.exitCode,\n timedOut: isInSandboxTimeoutExit(result.exitCode),\n };\n}\n\nfunction createCloudflareProgrammaticToolCallingSchema(\n config: t.CloudflareSandboxExecutionConfig\n): CloudflareProgrammaticToolCallingJsonSchema {\n return {\n ...ProgrammaticToolCallingSchema,\n properties: {\n ...ProgrammaticToolCallingSchema.properties,\n timeout: createTimeoutSchema(config.timeoutMs),\n lang: {\n type: 'string',\n enum: ['py', 'python', 'bash', 'sh'],\n default: 'bash',\n description:\n 'Cloudflare Sandbox runtime for orchestration code. Defaults to bash; use py/python for Python orchestration.',\n },\n },\n } as const;\n}\n\nfunction createCloudflareBashProgrammaticToolCallingSchema(\n config: t.CloudflareSandboxExecutionConfig\n): CloudflareBashProgrammaticToolCallingJsonSchema {\n return {\n ...BashProgrammaticToolCallingSchema,\n properties: {\n ...BashProgrammaticToolCallingSchema.properties,\n timeout: createTimeoutSchema(config.timeoutMs),\n },\n } as const;\n}\n\nfunction resolveRuntime(params: ProgrammaticParams): 'python' | 'bash' {\n const raw = params.lang ?? params.runtime ?? params.language ?? 'bash';\n return raw === 'py' || raw === 'python' ? 'python' : 'bash';\n}\n\nfunction filterNativeTools(\n toolDefs: t.LCTool[],\n code: string,\n runtime: 'python' | 'bash'\n): t.LCTool[] {\n const nativeDefs = toolDefs.filter((def) => NATIVE_TOOL_NAMES.has(def.name));\n const filter =\n runtime === 'bash' ? filterBashToolsByUsage : filterToolsByUsage;\n return filter(nativeDefs, code);\n}\n\nfunction indent(code: string, spaces = 4): string {\n const prefix = ' '.repeat(spaces);\n return code\n .split('\\n')\n .map((line) => (line === '' ? line : prefix + line))\n .join('\\n');\n}\n\nfunction pythonBoolean(value: boolean | undefined): 'True' | 'False' {\n return value === true ? 'True' : 'False';\n}\n\nfunction createPythonNativeToolSource(\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n return `\nimport asyncio, fnmatch, glob, json, os, pathlib, re, shlex, shutil, subprocess, sys, tempfile\n\nWORKSPACE = ${JSON.stringify(workspaceRoot)}\nSHELL = ${JSON.stringify(config.shell ?? 'bash')}\nREAD_ONLY = ${pythonBoolean(config.readOnly)}\nALLOW_DANGEROUS_COMMANDS = ${pythonBoolean(config.allowDangerousCommands)}\nDESTRUCTIVE_TARGET = r\"(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:/?\\\\.?\\\\*|/)?\"\nDANGEROUS_COMMAND_PATTERNS = [\n re.compile(r\"\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+|-[^\\\\s]*[r][^\\\\s]*\\\\s+-[^\\\\s]*[f][^\\\\s]*\\\\s+)(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"\\\\s*(?:$|[;&|])\"),\n re.compile(r\"\\\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\\\b\"),\n re.compile(r\"\\\\bdd\\\\s+[^;&|]*\\\\bof=/dev/\"),\n re.compile(r\"\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"(?:$|\\\\s|[;&|])\"),\n re.compile(r\"\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?\" + DESTRUCTIVE_TARGET + r\"(?:$|\\\\s|[;&|])\"),\n re.compile(r\":\\\\s*\\\\(\\\\s*\\\\)\\\\s*\\\\{\\\\s*:\\\\s*\\\\|\\\\s*:\\\\s*&\\\\s*\\\\}\\\\s*;\\\\s*:\"),\n]\nQUOTED_DESTRUCTIVE_PATTERNS = [\n re.compile(r\"\\\\brm\\\\s+(?:-[^\\\\s]*[rf][^\\\\s]*\\\\s+){1,3}(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n re.compile(r\"\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n re.compile(r\"\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?[\\\\\"']\" + DESTRUCTIVE_TARGET + r\"[\\\\\"']\"),\n]\nNESTED_SHELL_PREFIX = r\"(?:(?:ba|z|da|k)?sh|eval)\\\\s+(?:-l?c\\\\s+)?\"\nNESTED_SHELL_DESTRUCTIVE_PATTERNS = [\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\brm\\\\s+-[^\\\\s\\\\\"']*[rf][^\\\\s\\\\\"']*\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\bchmod\\\\s+-R\\\\s+(?:777|a\\\\+w)\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n re.compile(NESTED_SHELL_PREFIX + r\"[\\\\\"'][^\\\\\"']*\\\\bchown\\\\s+-R\\\\s+[^;&|]+\\\\s+(?:--\\\\s+)?(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)\"),\n]\nMUTATING_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*\")\nPROTECTED_TARGET_ARG_RE = re.compile(r\"^(?:/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:/?\\\\.?\\\\*|/)?$\")\nDESTRUCTIVE_OP_IN_COMMAND_RE = re.compile(r\"\\\\b(?:rm\\\\s+-[^\\\\s]*[rf]|chmod\\\\s+-R|chown\\\\s+-R)\\\\b\")\n\ndef _is_within_workspace(file_path):\n resolved = os.path.abspath(file_path)\n root = os.path.abspath(WORKSPACE)\n return os.path.commonpath([root, resolved]) == root\n\ndef _resolve(file_path=\".\"):\n raw = file_path or \".\"\n candidate = raw if os.path.isabs(raw) else os.path.join(WORKSPACE, raw)\n resolved = os.path.abspath(candidate)\n if not _is_within_workspace(resolved):\n raise ValueError(f\"Path is outside the Cloudflare sandbox workspace: {file_path}\")\n return resolved\n\ndef _assert_writable(tool_name):\n if READ_ONLY:\n raise PermissionError(f\"{tool_name} is blocked in read-only Cloudflare sandbox mode.\")\n\ndef _strip_quoted_content(command):\n output = []\n quote = None\n escaped = False\n index = 0\n while index < len(command):\n char = command[index]\n if escaped:\n escaped = False\n output.append(\" \")\n index += 1\n continue\n if char == \"\\\\\\\\\":\n escaped = True\n output.append(\" \")\n index += 1\n continue\n if quote is not None:\n if char == quote:\n quote = None\n output.append(\" \")\n index += 1\n continue\n if char in (\"'\", '\"', \"\\`\"):\n quote = char\n output.append(\" \")\n index += 1\n continue\n if char == \"#\":\n while index < len(command) and command[index] != \"\\\\n\":\n output.append(\" \")\n index += 1\n output.append(\"\\\\n\")\n index += 1\n continue\n output.append(char)\n index += 1\n return \"\".join(output)\n\ndef _validate_bash_command(command, args=None):\n errors = []\n normalized = _strip_quoted_content(command)\n if command.strip() == \"\":\n errors.append(\"Command is empty.\")\n if \"\\\\0\" in command:\n errors.append(\"Command contains a NUL byte.\")\n if not ALLOW_DANGEROUS_COMMANDS:\n if any(pattern.search(normalized) for pattern in DANGEROUS_COMMAND_PATTERNS):\n errors.append(\"Command matches a destructive command pattern.\")\n elif any(pattern.search(command) for pattern in QUOTED_DESTRUCTIVE_PATTERNS):\n errors.append(\"Command matches a destructive command pattern (quoted target).\")\n elif any(pattern.search(command) for pattern in NESTED_SHELL_DESTRUCTIVE_PATTERNS):\n errors.append(\"Command matches a destructive command pattern (nested shell payload).\")\n elif args and DESTRUCTIVE_OP_IN_COMMAND_RE.search(command):\n offending = next((str(arg) for arg in args if PROTECTED_TARGET_ARG_RE.search(str(arg))), None)\n if offending is not None:\n errors.append(f\"Command matches a destructive command pattern (protected target \\\\\"{offending}\\\\\" passed via positional arg).\")\n if READ_ONLY and MUTATING_COMMAND_PATTERN.search(normalized):\n errors.append(\"Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.\")\n if errors:\n raise ValueError(\"\\\\n\".join(errors))\n\ndef _line_window(content, offset=None, limit=None):\n start = max((offset or 1) - 1, 0)\n lines = content.split(\"\\\\n\")\n selected = lines[start:] if not limit or limit <= 0 else lines[start:start + limit]\n return \"\\\\n\".join(f\"{start + idx + 1:6d}\\\\t{line}\" for idx, line in enumerate(selected))\n\ndef _run(command, timeout=None, args=None):\n _validate_bash_command(command, args=args)\n completed = subprocess.run(\n [SHELL, \"-lc\", command, \"--\"] + [str(arg) for arg in (args or [])],\n cwd=WORKSPACE,\n capture_output=True,\n text=True,\n timeout=(timeout / 1000 if timeout else None),\n )\n return {\n \"stdout\": completed.stdout,\n \"stderr\": completed.stderr,\n \"exit_code\": completed.returncode,\n }\n\ndef _format_run(result):\n text = \"\"\n if result.get(\"stdout\"):\n text += f\"stdout:\\\\n{result['stdout']}\\\\n\"\n else:\n text += \"stdout: Empty. Ensure you're writing output explicitly.\\\\n\"\n if result.get(\"stderr\"):\n text += f\"stderr:\\\\n{result['stderr']}\\\\n\"\n if result.get(\"exit_code\") not in (None, 0):\n text += f\"exit_code: {result['exit_code']}\\\\n\"\n text += f\"working_directory: {WORKSPACE}\"\n return text.strip()\n\ndef _detect_compile_command():\n if os.path.exists(os.path.join(WORKSPACE, \"tsconfig.json\")):\n return \"typescript\", \"npx --no-install tsc --noEmit\", \"tsconfig.json present\"\n package_json = os.path.join(WORKSPACE, \"package.json\")\n if os.path.exists(package_json):\n try:\n if '\"typescript\"' in open(package_json, encoding=\"utf-8\").read():\n return \"typescript\", \"npx --no-install tsc --noEmit\", \"package.json declares typescript\"\n except Exception:\n pass\n if os.path.exists(os.path.join(WORKSPACE, \"Cargo.toml\")):\n return \"rust\", \"cargo check --message-format=short\", \"Cargo.toml present\"\n if os.path.exists(os.path.join(WORKSPACE, \"go.mod\")):\n return \"go\", \"go vet ./...\", \"go.mod present\"\n if any(os.path.exists(os.path.join(WORKSPACE, name)) for name in [\"pyproject.toml\", \"setup.py\", \"setup.cfg\"]):\n return \"python-compile\", \"python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')\", \"Python project\"\n return \"unknown\", \"\", \"no recognised project marker\"\n\nasync def bash_tool(command, args=None):\n return _format_run(_run(command, args=args))\n\nasync def execute_code(lang, code, args=None):\n args = args or []\n temp_dir = tempfile.mkdtemp(prefix=\"lc-ptc-\", dir=WORKSPACE)\n try:\n def q(value):\n import shlex\n return shlex.quote(str(value))\n arg_text = \" \".join(q(arg) for arg in args)\n if lang in (\"py\", \"python\"):\n file_path = os.path.join(temp_dir, \"main.py\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"python3 {q(file_path)} {arg_text}\"))\n if lang in (\"js\", \"javascript\"):\n file_path = os.path.join(temp_dir, \"main.js\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"node {q(file_path)} {arg_text}\"))\n if lang in (\"ts\", \"typescript\"):\n file_path = os.path.join(temp_dir, \"main.ts\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"npx --no-install tsx {q(file_path)} {arg_text}\"))\n if lang == \"php\":\n file_path = os.path.join(temp_dir, \"main.php\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"php {q(file_path)} {arg_text}\"))\n if lang == \"go\":\n file_path = os.path.join(temp_dir, \"main.go\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"go run {q(file_path)} {arg_text}\"))\n if lang == \"rs\":\n file_path = os.path.join(temp_dir, \"main.rs\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-rs\")\n return _format_run(_run(f\"rustc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"c\":\n file_path = os.path.join(temp_dir, \"main.c\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-c\")\n return _format_run(_run(f\"cc {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"cpp\":\n file_path = os.path.join(temp_dir, \"main.cpp\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-cpp\")\n return _format_run(_run(f\"c++ {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"java\":\n file_path = os.path.join(temp_dir, \"Main.java\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"javac {q(file_path)} && java -cp {q(temp_dir)} Main {arg_text}\"))\n if lang == \"r\":\n file_path = os.path.join(temp_dir, \"main.R\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n return _format_run(_run(f\"Rscript {q(file_path)} {arg_text}\"))\n if lang == \"d\":\n file_path = os.path.join(temp_dir, \"main.d\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-d\")\n return _format_run(_run(f\"dmd {q(file_path)} -of={q(binary)} && {q(binary)} {arg_text}\"))\n if lang == \"f90\":\n file_path = os.path.join(temp_dir, \"main.f90\")\n open(file_path, \"w\", encoding=\"utf-8\").write(code)\n binary = os.path.join(temp_dir, \"main-f90\")\n return _format_run(_run(f\"gfortran {q(file_path)} -o {q(binary)} && {q(binary)} {arg_text}\"))\n if lang in (\"bash\", \"sh\"):\n return _format_run(_run(code, args=args))\n raise ValueError(f\"Unsupported Cloudflare sandbox runtime: {lang}\")\n finally:\n shutil.rmtree(temp_dir, ignore_errors=True)\n\nasync def read_file(path, offset=None, limit=None):\n resolved = _resolve(path)\n with open(resolved, encoding=\"utf-8\") as handle:\n return _line_window(handle.read(), offset, limit)\n\nasync def write_file(path, content):\n _assert_writable(\"write_file\")\n resolved = _resolve(path)\n os.makedirs(os.path.dirname(resolved), exist_ok=True)\n existed = os.path.exists(resolved)\n with open(resolved, \"w\", encoding=\"utf-8\") as handle:\n handle.write(content)\n return f\"{'Overwrote' if existed else 'Created'} {resolved} ({len(content)} chars).\"\n\nasync def edit_file(path, old_text=None, new_text=None, edits=None):\n _assert_writable(\"edit_file\")\n resolved = _resolve(path)\n edits = edits or [{\"old_text\": old_text, \"new_text\": new_text}]\n content = open(resolved, encoding=\"utf-8\").read()\n for edit in edits:\n old = edit.get(\"old_text\") or \"\"\n new = edit.get(\"new_text\") or \"\"\n if content.count(old) != 1:\n raise ValueError(f\"Could not locate old_text exactly once in {path}\")\n content = content.replace(old, new, 1)\n open(resolved, \"w\", encoding=\"utf-8\").write(content)\n return f\"Applied {len(edits)} edit(s) to {resolved}.\"\n\nasync def list_directory(path=\".\"):\n resolved = _resolve(path)\n entries = []\n for name in sorted(os.listdir(resolved)):\n full = os.path.join(resolved, name)\n entries.append((\"dir \" if os.path.isdir(full) else \"file\") + \"\\\\t\" + name)\n return \"\\\\n\".join(entries) or \"Directory is empty.\"\n\nasync def grep_search(pattern, path=\".\", glob=None, max_results=200):\n root = _resolve(path)\n regex = re.compile(pattern)\n out = []\n for current, dirs, files in os.walk(root):\n dirs[:] = [d for d in dirs if d not in {\".git\", \"node_modules\", \".venv\", \"dist\", \"build\"}]\n for name in files:\n rel = os.path.relpath(os.path.join(current, name), root)\n if glob and not fnmatch.fnmatch(rel, glob):\n continue\n try:\n for line_no, line in enumerate(open(os.path.join(current, name), encoding=\"utf-8\", errors=\"ignore\"), 1):\n if regex.search(line):\n out.append(f\"{os.path.join(current, name)}:{line_no}:{line.rstrip()}\")\n if len(out) >= max_results:\n return \"\\\\n\".join(out)\n except Exception:\n pass\n return \"\\\\n\".join(out) if out else \"No matches found.\"\n\nasync def glob_search(pattern, path=\".\", max_results=200):\n root = _resolve(path)\n target = pattern if os.path.isabs(pattern) else os.path.join(root, pattern)\n matches = []\n for match in glob_module.glob(target, recursive=True):\n resolved = os.path.abspath(match)\n if _is_within_workspace(resolved):\n matches.append(resolved)\n if len(matches) >= max_results:\n break\n return \"\\\\n\".join(matches) if matches else \"No files found.\"\n\nasync def compile_check(command=None, timeout_ms=None):\n kind, detected, reason = _detect_compile_command()\n command = command or detected\n if not command:\n return f\"compile_check: {reason}. Pass an explicit command to override.\"\n result = _run(command, timeout_ms)\n status = \"PASSED\" if result[\"exit_code\"] == 0 else \"FAILED\"\n return f\"compile_check ({kind}) {status} via {command}\\\\n\\\\nstdout:\\\\n{result['stdout']}\\\\nstderr:\\\\n{result['stderr']}\\\\nworking_directory: {WORKSPACE}\\\\nreason: {reason}\"\n\n# Avoid shadowing the glob_search function argument named \"glob\".\nglob_module = glob\n`.trim();\n}\n\nfunction createNodeNativeToolSource(\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n return `\nconst fs = require(\"fs\");\nconst fsp = fs.promises;\nconst path = require(\"path\");\nconst cp = require(\"child_process\");\n\nconst WORKSPACE = ${JSON.stringify(workspaceRoot)};\nconst SHELL = ${JSON.stringify(config.shell ?? 'bash')};\nconst READ_ONLY = ${JSON.stringify(config.readOnly === true)};\nconst ALLOW_DANGEROUS_COMMANDS = ${JSON.stringify(config.allowDangerousCommands === true)};\nconst DESTRUCTIVE_TARGET = \"(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)(?:\\\\\\\\/?\\\\\\\\.?\\\\\\\\*|\\\\\\\\/)?\";\nconst DANGEROUS_COMMAND_PATTERNS = [\n new RegExp(\"\\\\\\\\brm\\\\\\\\s+(?:-[^\\\\\\\\s]*[rf][^\\\\\\\\s]*\\\\\\\\s+|-[^\\\\\\\\s]*[r][^\\\\\\\\s]*\\\\\\\\s+-[^\\\\\\\\s]*[f][^\\\\\\\\s]*\\\\\\\\s+)(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"\\\\\\\\s*(?:$|[;&|])\"),\n /\\\\b(?:mkfs|mkswap|fdisk|parted|diskutil)\\\\b/,\n /\\\\bdd\\\\s+[^;&|]*\\\\bof=\\\\/dev\\\\//,\n new RegExp(\"\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"(?:$|\\\\\\\\s|[;&|])\"),\n new RegExp(\"\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?\" + DESTRUCTIVE_TARGET + \"(?:$|\\\\\\\\s|[;&|])\"),\n /:\\\\s*\\\\(\\\\s*\\\\)\\\\s*\\\\{\\\\s*:\\\\s*\\\\|\\\\s*:\\\\s*&\\\\s*\\\\}\\\\s*;\\\\s*:/,\n];\nconst QUOTED_DESTRUCTIVE_PATTERNS = [\n new RegExp(\"\\\\\\\\brm\\\\\\\\s+(?:-[^\\\\\\\\s]*[rf][^\\\\\\\\s]*\\\\\\\\s+){1,3}(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n new RegExp(\"\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n new RegExp(\"\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?[\\\\\\\"']\" + DESTRUCTIVE_TARGET + \"[\\\\\\\"']\"),\n];\nconst NESTED_SHELL_PREFIX = \"(?:(?:ba|z|da|k)?sh|eval)\\\\\\\\s+(?:-l?c\\\\\\\\s+)?\";\nconst NESTED_SHELL_DESTRUCTIVE_PATTERNS = [\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\brm\\\\\\\\s+-[^\\\\\\\\s\\\\\\\"']*[rf][^\\\\\\\\s\\\\\\\"']*\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\bchmod\\\\\\\\s+-R\\\\\\\\s+(?:777|a\\\\\\\\+w)\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n new RegExp(NESTED_SHELL_PREFIX + \"[\\\\\\\"'][^\\\\\\\"']*\\\\\\\\bchown\\\\\\\\s+-R\\\\\\\\s+[^;&|]+\\\\\\\\s+(?:--\\\\\\\\s+)?(?:\\\\\\\\/|~|\\\\\\\\$\\\\\\\\{?HOME\\\\\\\\}?|\\\\\\\\.)\"),\n];\nconst 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*/;\nconst PROTECTED_TARGET_ARG_RE = /^(?:\\\\/|~|\\\\$\\\\{?HOME\\\\}?|\\\\.)(?:\\\\/?\\\\.?\\\\*|\\\\/)?$/;\nconst DESTRUCTIVE_OP_IN_COMMAND_RE = /\\\\b(?:rm\\\\s+-[^\\\\s]*[rf]|chmod\\\\s+-R|chown\\\\s+-R)\\\\b/;\n\nfunction resolvePath(filePath) {\n const raw = filePath || \".\";\n const candidate = path.isAbsolute(raw) ? raw : path.join(WORKSPACE, raw);\n const resolved = path.resolve(candidate);\n const root = path.resolve(WORKSPACE);\n const relative = path.relative(root, resolved);\n if (relative && (relative.startsWith(\"..\") || path.isAbsolute(relative))) {\n throw new Error(\"Path is outside the Cloudflare sandbox workspace: \" + filePath);\n }\n return resolved;\n}\n\nfunction assertWritable(toolName) {\n if (READ_ONLY) {\n throw new Error(toolName + \" is blocked in read-only Cloudflare sandbox mode.\");\n }\n}\n\nfunction stripQuotedContent(command) {\n let output = \"\";\n let quote;\n let escaped = false;\n for (let index = 0; index < command.length; index++) {\n const char = command[index];\n if (escaped) {\n escaped = false;\n output += \" \";\n continue;\n }\n if (char === \"\\\\\\\\\") {\n escaped = true;\n output += \" \";\n continue;\n }\n if (quote != null) {\n if (char === quote) quote = undefined;\n output += \" \";\n continue;\n }\n if (char === \"\\\\\\\"\" || char === \"'\" || char === \"\\`\") {\n quote = char;\n output += \" \";\n continue;\n }\n if (char === \"#\") {\n while (index < command.length && command[index] !== \"\\\\n\") {\n output += \" \";\n index += 1;\n }\n output += \"\\\\n\";\n continue;\n }\n output += char;\n }\n return output;\n}\n\nfunction validateBashCommand(command, args) {\n const errors = [];\n const normalized = stripQuotedContent(command);\n if (command.trim() === \"\") {\n errors.push(\"Command is empty.\");\n }\n if (command.includes(\"\\\\0\")) {\n errors.push(\"Command contains a NUL byte.\");\n }\n if (!ALLOW_DANGEROUS_COMMANDS) {\n if (DANGEROUS_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {\n errors.push(\"Command matches a destructive command pattern.\");\n } else if (QUOTED_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {\n errors.push(\"Command matches a destructive command pattern (quoted target).\");\n } else if (NESTED_SHELL_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(command))) {\n errors.push(\"Command matches a destructive command pattern (nested shell payload).\");\n } else if ((args || []).length > 0 && DESTRUCTIVE_OP_IN_COMMAND_RE.test(command)) {\n const offending = (args || []).map(String).find((arg) => PROTECTED_TARGET_ARG_RE.test(arg));\n if (offending !== undefined) {\n errors.push(\"Command matches a destructive command pattern (protected target \\\\\"\" + offending + \"\\\\\" passed via positional arg).\");\n }\n }\n }\n if (READ_ONLY && MUTATING_COMMAND_PATTERN.test(normalized)) {\n errors.push(\"Command appears to mutate files or repository state in read-only Cloudflare sandbox mode.\");\n }\n if (errors.length > 0) {\n throw new Error(errors.join(\"\\\\n\"));\n }\n}\n\nfunction lineWindow(content, offset, limit) {\n const start = Math.max((offset || 1) - 1, 0);\n const lines = content.split(\"\\\\n\");\n const selected = !limit || limit <= 0 ? lines.slice(start) : lines.slice(start, start + limit);\n return selected.map((line, index) => String(start + index + 1).padStart(6, \" \") + \"\\\\t\" + line).join(\"\\\\n\");\n}\n\nfunction quote(value) {\n const text = String(value);\n if (text === \"\") return \"''\";\n if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(text)) return text;\n return \"'\" + text.replace(/'/g, \"'\\\\\\\\''\") + \"'\";\n}\n\nfunction run(command, timeoutMs, args) {\n validateBashCommand(command, args);\n return new Promise((resolve) => {\n const child = cp.spawn(SHELL, [\"-lc\", command, \"--\", ...((args || []).map(String))], {\n cwd: WORKSPACE,\n env: process.env,\n });\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n const timer = timeoutMs\n ? setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n }, timeoutMs)\n : null;\n\n child.stdout.on(\"data\", (chunk) => {\n stdout += chunk.toString();\n });\n child.stderr.on(\"data\", (chunk) => {\n stderr += chunk.toString();\n });\n child.on(\"error\", (error) => {\n if (timer) clearTimeout(timer);\n resolve({ stdout, stderr: stderr + error.message, exit_code: 1, timed_out: timedOut });\n });\n child.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n resolve({ stdout, stderr, exit_code: timedOut ? null : code, timed_out: timedOut });\n });\n });\n}\n\nfunction formatRun(result) {\n let text = \"\";\n if (result.stdout) {\n text += \"stdout:\\\\n\" + result.stdout + \"\\\\n\";\n } else {\n text += \"stdout: Empty. Ensure you're writing output explicitly.\\\\n\";\n }\n if (result.stderr) {\n text += \"stderr:\\\\n\" + result.stderr + \"\\\\n\";\n }\n if (result.timed_out) {\n text += \"timed_out: true\\\\n\";\n }\n if (result.exit_code !== null && result.exit_code !== undefined && result.exit_code !== 0) {\n text += \"exit_code: \" + result.exit_code + \"\\\\n\";\n }\n text += \"working_directory: \" + WORKSPACE;\n return text.trim();\n}\n\nasync function detectCompileCommand() {\n async function exists(name) {\n try {\n await fsp.access(path.join(WORKSPACE, name));\n return true;\n } catch {\n return false;\n }\n }\n if (await exists(\"tsconfig.json\")) {\n return [\"typescript\", \"npx --no-install tsc --noEmit\", \"tsconfig.json present\"];\n }\n if (await exists(\"package.json\")) {\n try {\n if ((await fsp.readFile(path.join(WORKSPACE, \"package.json\"), \"utf8\")).includes('\"typescript\"')) {\n return [\"typescript\", \"npx --no-install tsc --noEmit\", \"package.json declares typescript\"];\n }\n } catch {}\n }\n if (await exists(\"Cargo.toml\")) return [\"rust\", \"cargo check --message-format=short\", \"Cargo.toml present\"];\n if (await exists(\"go.mod\")) return [\"go\", \"go vet ./...\", \"go.mod present\"];\n if (await exists(\"pyproject.toml\") || await exists(\"setup.py\") || await exists(\"setup.cfg\")) {\n return [\"python-compile\", \"python3 -m py_compile $(find . -name '*.py' -not -path './.venv/*' -not -path './node_modules/*')\", \"Python project\"];\n }\n return [\"unknown\", \"\", \"no recognised project marker\"];\n}\n\nfunction globToRegExp(pattern) {\n const escaped = pattern.replace(/[|\\\\\\\\{}()[\\\\]^$+*?.]/g, \"\\\\\\\\$&\");\n return new RegExp(\"^\" + escaped.replace(/\\\\\\\\\\\\*\\\\\\\\\\\\*/g, \".*\").replace(/\\\\\\\\\\\\*/g, \"[^/]*\") + \"$\");\n}\n\nfunction globMatch(relativePath, pattern) {\n const matcher = globToRegExp(pattern);\n return matcher.test(relativePath) || matcher.test(path.basename(relativePath));\n}\n\nasync function walkFiles(root, visit) {\n const entries = await fsp.readdir(root, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(root, entry.name);\n if (entry.isDirectory()) {\n if ([\".git\", \"node_modules\", \".venv\", \"dist\", \"build\"].includes(entry.name)) continue;\n await walkFiles(full, visit);\n } else if (entry.isFile()) {\n await visit(full);\n }\n }\n}\n\nasync function bash_tool(payload) {\n return formatRun(await run(payload.command, undefined, payload.args));\n}\n\nasync function execute_code(payload) {\n const lang = payload.lang;\n const code = payload.code;\n const args = payload.args || [];\n const tempDir = await fsp.mkdtemp(path.join(WORKSPACE, \"lc-ptc-\"));\n try {\n const argText = args.map(quote).join(\" \");\n async function writeAndRun(fileName, command) {\n const filePath = path.join(tempDir, fileName);\n await fsp.writeFile(filePath, code, \"utf8\");\n return formatRun(await run(command(filePath, argText), undefined, []));\n }\n if (lang === \"py\" || lang === \"python\") {\n return writeAndRun(\"main.py\", (filePath, argText) => \"python3 \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"js\" || lang === \"javascript\") {\n return writeAndRun(\"main.js\", (filePath, argText) => \"node \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"ts\" || lang === \"typescript\") {\n return writeAndRun(\"main.ts\", (filePath, argText) => \"npx --no-install tsx \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"php\") {\n return writeAndRun(\"main.php\", (filePath, argText) => \"php \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"go\") {\n return writeAndRun(\"main.go\", (filePath, argText) => \"go run \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"rs\") {\n return writeAndRun(\"main.rs\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-rs\");\n return \"rustc \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"c\") {\n return writeAndRun(\"main.c\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-c\");\n return \"cc \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"cpp\") {\n return writeAndRun(\"main.cpp\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-cpp\");\n return \"c++ \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"java\") {\n return writeAndRun(\"Main.java\", (filePath, argText) => \"javac \" + quote(filePath) + \" && java -cp \" + quote(tempDir) + \" Main \" + argText);\n }\n if (lang === \"r\") {\n return writeAndRun(\"main.R\", (filePath, argText) => \"Rscript \" + quote(filePath) + \" \" + argText);\n }\n if (lang === \"d\") {\n return writeAndRun(\"main.d\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-d\");\n return \"dmd \" + quote(filePath) + \" -of=\" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"f90\") {\n return writeAndRun(\"main.f90\", (filePath, argText) => {\n const binary = path.join(tempDir, \"main-f90\");\n return \"gfortran \" + quote(filePath) + \" -o \" + quote(binary) + \" && \" + quote(binary) + \" \" + argText;\n });\n }\n if (lang === \"bash\" || lang === \"sh\") {\n return formatRun(await run(code, undefined, args));\n }\n throw new Error(\"Unsupported Cloudflare sandbox runtime: \" + lang);\n } finally {\n await fsp.rm(tempDir, { recursive: true, force: true });\n }\n}\n\nasync function read_file(payload) {\n const resolved = resolvePath(payload.path);\n return lineWindow(await fsp.readFile(resolved, \"utf8\"), payload.offset, payload.limit);\n}\n\nasync function write_file(payload) {\n assertWritable(\"write_file\");\n const resolved = resolvePath(payload.path);\n await fsp.mkdir(path.dirname(resolved), { recursive: true });\n const existed = fs.existsSync(resolved);\n await fsp.writeFile(resolved, payload.content, \"utf8\");\n return (existed ? \"Overwrote \" : \"Created \") + resolved + \" (\" + payload.content.length + \" chars).\";\n}\n\nasync function edit_file(payload) {\n assertWritable(\"edit_file\");\n const resolved = resolvePath(payload.path);\n const edits = payload.edits || [{ old_text: payload.old_text, new_text: payload.new_text }];\n let content = await fsp.readFile(resolved, \"utf8\");\n for (const edit of edits) {\n const oldText = edit.old_text || \"\";\n const newText = edit.new_text || \"\";\n if (oldText === \"\" || content.split(oldText).length - 1 !== 1) {\n throw new Error(\"Could not locate old_text exactly once in \" + payload.path);\n }\n content = content.replace(oldText, newText);\n }\n await fsp.writeFile(resolved, content, \"utf8\");\n return \"Applied \" + edits.length + \" edit(s) to \" + resolved + \".\";\n}\n\nasync function list_directory(payload) {\n const resolved = resolvePath(payload.path || \".\");\n const entries = await fsp.readdir(resolved, { withFileTypes: true });\n const lines = entries\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((entry) => (entry.isDirectory() ? \"dir\" : \"file\") + \"\\\\t\" + entry.name);\n return lines.join(\"\\\\n\") || \"Directory is empty.\";\n}\n\nasync function grep_search(payload) {\n const root = resolvePath(payload.path || \".\");\n const regex = new RegExp(payload.pattern);\n const maxResults = payload.max_results || 200;\n const out = [];\n await walkFiles(root, async (filePath) => {\n if (out.length >= maxResults) return;\n const relative = path.relative(root, filePath);\n if (payload.glob && !globMatch(relative, payload.glob)) return;\n let text = \"\";\n try {\n text = await fsp.readFile(filePath, \"utf8\");\n } catch {\n return;\n }\n text.split(\"\\\\n\").forEach((line, index) => {\n if (out.length < maxResults && regex.test(line)) {\n out.push(filePath + \":\" + (index + 1) + \":\" + line);\n }\n });\n });\n return out.join(\"\\\\n\") || \"No matches found.\";\n}\n\nasync function glob_search(payload) {\n const root = resolvePath(payload.path || \".\");\n const maxResults = payload.max_results || 200;\n const out = [];\n await walkFiles(root, async (filePath) => {\n if (out.length >= maxResults) return;\n const relative = path.relative(root, filePath);\n if (globMatch(relative, payload.pattern)) out.push(filePath);\n });\n return out.join(\"\\\\n\") || \"No files found.\";\n}\n\nasync function compile_check(payload) {\n const [kind, detected, reason] = await detectCompileCommand();\n const command = payload.command || detected;\n if (!command) {\n return \"compile_check: \" + reason + \". Pass an explicit command to override.\";\n }\n const result = await run(command, payload.timeout_ms);\n const status = result.exit_code === 0 ? \"PASSED\" : \"FAILED\";\n return \"compile_check (\" + kind + \") \" + status + \" via \" + command + \"\\\\n\\\\nstdout:\\\\n\" + result.stdout + \"\\\\nstderr:\\\\n\" + result.stderr + \"\\\\nworking_directory: \" + WORKSPACE + \"\\\\nreason: \" + reason;\n}\n\nconst TOOLS = {\n bash_tool,\n execute_code,\n read_file,\n write_file,\n edit_file,\n list_directory,\n grep_search,\n glob_search,\n compile_check,\n};\n\nasync function main() {\n const name = process.argv[2];\n const payload = JSON.parse(process.argv[3] || \"{}\");\n if (!TOOLS[name]) throw new Error(\"Unknown tool: \" + name);\n const result = await TOOLS[name](payload);\n process.stdout.write(typeof result === \"string\" ? result : JSON.stringify(result));\n}\n\nmain().catch((error) => {\n console.error(error && error.stack ? error.stack : String(error));\n process.exit(1);\n});\n`.trim();\n}\n\nfunction createPythonProgram(\n userCode: string,\n toolDefs: t.LCTool[],\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n const aliases = toolDefs\n .map((def) => {\n const pythonName = normalizeToPythonIdentifier(def.name);\n return NATIVE_TOOL_NAMES.has(def.name) && pythonName !== def.name\n ? `${pythonName} = globals()[${JSON.stringify(def.name)}]`\n : '';\n })\n .filter(Boolean)\n .join('\\n');\n return `${createPythonNativeToolSource(config, workspaceRoot)}\n${aliases}\n\nasync def __lc_user_main__():\n${indent(userCode)}\n\nasyncio.run(__lc_user_main__())\n`;\n}\n\nfunction createBashProgram(\n userCode: string,\n toolDefs: t.LCTool[],\n config: t.CloudflareSandboxExecutionConfig,\n workspaceRoot: string\n): string {\n const helper = createNodeNativeToolSource(config, workspaceRoot);\n const functions = toolDefs\n .map((def) => {\n const bashName = normalizeToBashIdentifier(def.name);\n if (!NATIVE_TOOL_NAMES.has(def.name)) {\n return '';\n }\n return `${bashName}() { node \"$__LC_TOOL_HELPER\" ${JSON.stringify(def.name)} \"$1\"; }`;\n })\n .filter(Boolean)\n .join('\\n');\n return `\nset -euo pipefail\ncommand -v node >/dev/null 2>&1 || { echo \"Cloudflare programmatic tool calling requires node in the sandbox image.\" >&2; exit 127; }\n__LC_TOOL_HELPER=\"$(mktemp /tmp/lc-tools.XXXXXX.js)\"\ncat > \"$__LC_TOOL_HELPER\" <<'JS'\n${helper}\nJS\ntrap 'rm -f \"$__LC_TOOL_HELPER\"' EXIT\n${functions}\n${userCode}\n`.trim();\n}\n\nasync function runProgrammatic(args: {\n params: ProgrammaticParams;\n config?: { toolCall?: unknown };\n cloudflareConfig: t.CloudflareSandboxExecutionConfig;\n runtime: 'python' | 'bash';\n}): Promise<[string, t.ProgrammaticExecutionArtifact]> {\n const toolCall = (args.config?.toolCall ??\n {}) as Partial<t.ProgrammaticCache>;\n const toolDefs = toolCall.toolDefs ?? [];\n const effectiveTools = filterNativeTools(\n toolDefs,\n args.params.code,\n args.runtime\n );\n const timeoutMs = clampExecutionTimeout(\n args.params.timeout,\n args.cloudflareConfig.timeoutMs\n );\n const workspaceRoot = getCloudflareWorkspaceRoot(args.cloudflareConfig);\n let result: Awaited<ReturnType<typeof executeCloudflareCode>>;\n\n if (args.runtime === 'bash') {\n await validateCloudflareBashCommand(args.params.code, [], {\n ...args.cloudflareConfig,\n timeoutMs,\n });\n result = await executeGeneratedCloudflareBash(\n createBashProgram(\n args.params.code,\n effectiveTools,\n args.cloudflareConfig,\n workspaceRoot\n ),\n { ...args.cloudflareConfig, timeoutMs }\n );\n } else {\n result = await executeCloudflareCode(\n {\n lang: 'py',\n code: createPythonProgram(\n args.params.code,\n effectiveTools,\n args.cloudflareConfig,\n workspaceRoot\n ),\n },\n { ...args.cloudflareConfig, timeoutMs }\n );\n }\n\n if (result.exitCode !== 0 || result.timedOut) {\n throw new Error(\n result.stderr !== ''\n ? result.stderr\n : `Cloudflare ${args.runtime} programmatic execution exited with code ${result.exitCode ?? 'unknown'}`\n );\n }\n\n return formatCompletedResponse({\n status: 'completed',\n session_id: 'cloudflare-sandbox',\n stdout: result.stdout,\n stderr: result.stderr,\n files: [],\n });\n}\n\nexport function createCloudflareProgrammaticToolCallingTool(\n cloudflareConfig: t.CloudflareSandboxExecutionConfig\n): DynamicStructuredTool {\n return tool(\n async (rawParams, config) => {\n const params = rawParams as ProgrammaticParams;\n return runProgrammatic({\n params,\n config,\n cloudflareConfig,\n runtime: resolveRuntime(params),\n });\n },\n {\n name: ProgrammaticToolCallingName,\n 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.`,\n schema: createCloudflareProgrammaticToolCallingSchema(cloudflareConfig),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport function createCloudflareBashProgrammaticToolCallingTool(\n cloudflareConfig: t.CloudflareSandboxExecutionConfig\n): DynamicStructuredTool {\n return tool(\n async (rawParams, config) => {\n const params = rawParams as ProgrammaticParams;\n return runProgrammatic({\n params,\n config,\n cloudflareConfig,\n runtime: 'bash',\n });\n },\n {\n name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,\n 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.`,\n schema:\n createCloudflareBashProgrammaticToolCallingSchema(cloudflareConfig),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n"],"mappings":"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"}
|
|
@@ -31,7 +31,7 @@ const LocalListDirectoryToolName = "list_directory";
|
|
|
31
31
|
const LocalReadFileToolSchema = {
|
|
32
32
|
type: "object",
|
|
33
33
|
properties: {
|
|
34
|
-
|
|
34
|
+
path: {
|
|
35
35
|
type: "string",
|
|
36
36
|
description: "Path to a local file, relative to the configured cwd unless absolute paths are allowed."
|
|
37
37
|
},
|
|
@@ -44,12 +44,12 @@ const LocalReadFileToolSchema = {
|
|
|
44
44
|
description: "Optional maximum number of lines to return."
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
required: ["
|
|
47
|
+
required: ["path"]
|
|
48
48
|
};
|
|
49
49
|
const LocalWriteFileToolSchema = {
|
|
50
50
|
type: "object",
|
|
51
51
|
properties: {
|
|
52
|
-
|
|
52
|
+
path: {
|
|
53
53
|
type: "string",
|
|
54
54
|
description: "Path to write, relative to the configured cwd unless absolute paths are allowed."
|
|
55
55
|
},
|
|
@@ -58,12 +58,12 @@ const LocalWriteFileToolSchema = {
|
|
|
58
58
|
description: "Complete file contents to write."
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
required: ["
|
|
61
|
+
required: ["path", "content"]
|
|
62
62
|
};
|
|
63
63
|
const LocalEditFileToolSchema = {
|
|
64
64
|
type: "object",
|
|
65
65
|
properties: {
|
|
66
|
-
|
|
66
|
+
path: {
|
|
67
67
|
type: "string",
|
|
68
68
|
description: "Path to edit, relative to the configured cwd unless absolute paths are allowed."
|
|
69
69
|
},
|
|
@@ -88,7 +88,7 @@ const LocalEditFileToolSchema = {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
|
-
required: ["
|
|
91
|
+
required: ["path"]
|
|
92
92
|
};
|
|
93
93
|
const LocalGrepSearchToolSchema = {
|
|
94
94
|
type: "object",
|
|
@@ -258,9 +258,9 @@ function createLocalReadFileTool(config = {}) {
|
|
|
258
258
|
const fs = require_LocalExecutionEngine.getWorkspaceFS(config);
|
|
259
259
|
return (0, _langchain_core_tools.tool)(async (rawInput) => {
|
|
260
260
|
const input = rawInput;
|
|
261
|
-
const path$4 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.
|
|
261
|
+
const path$4 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.path, config, "read");
|
|
262
262
|
const fileStat = await fs.stat(path$4);
|
|
263
|
-
if (!fileStat.isFile()) throw new Error(`Path is not a file: ${input.
|
|
263
|
+
if (!fileStat.isFile()) throw new Error(`Path is not a file: ${input.path}`);
|
|
264
264
|
const maxBytes = Math.max(config.maxReadBytes ?? DEFAULT_MAX_READ_BYTES, 1);
|
|
265
265
|
if (fileStat.size > maxBytes) return [`File is ${fileStat.size} bytes, exceeds the ${maxBytes}-byte read cap. Read a slice via bash (e.g. head/sed) or raise local.maxReadBytes.`, {
|
|
266
266
|
path: path$4,
|
|
@@ -330,7 +330,7 @@ function createLocalWriteFileTool(config = {}, checkpointer) {
|
|
|
330
330
|
return (0, _langchain_core_tools.tool)(async (rawInput) => {
|
|
331
331
|
const input = rawInput;
|
|
332
332
|
if (config.readOnly === true) throw new Error("write_file is blocked in read-only local mode.");
|
|
333
|
-
const path$5 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.
|
|
333
|
+
const path$5 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.path, config, "write");
|
|
334
334
|
if (checkpointer != null) await checkpointer.captureBeforeWrite(path$5);
|
|
335
335
|
let before = "";
|
|
336
336
|
let encoding = {
|
|
@@ -379,7 +379,7 @@ function createLocalEditFileTool(config = {}, checkpointer) {
|
|
|
379
379
|
if (config.readOnly === true) throw new Error("edit_file is blocked in read-only local mode.");
|
|
380
380
|
const edits = normalizeEdits(input);
|
|
381
381
|
if (edits.length === 0) throw new Error("edit_file requires old_text/new_text or edits[].");
|
|
382
|
-
const path$6 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.
|
|
382
|
+
const path$6 = await require_LocalExecutionEngine.resolveWorkspacePathSafe(input.path, config, "write");
|
|
383
383
|
const encoding = require_textEncoding.decodeFile(await fs.readFile(path$6, "utf8"));
|
|
384
384
|
const original = encoding.text;
|
|
385
385
|
let next = original;
|
|
@@ -387,7 +387,7 @@ function createLocalEditFileTool(config = {}, checkpointer) {
|
|
|
387
387
|
for (let i = 0; i < edits.length; i++) {
|
|
388
388
|
const edit = edits[i];
|
|
389
389
|
const match = require_editStrategies.locateEdit(next, edit.oldText);
|
|
390
|
-
if (match == null) throw new Error(`Edit ${i + 1}/${edits.length}: could not locate old_text in ${input.
|
|
390
|
+
if (match == null) throw new Error(`Edit ${i + 1}/${edits.length}: could not locate old_text in ${input.path}. Tried exact, line-trimmed, whitespace-normalized, and indentation-flexible matching. Re-read the file and copy the literal lines.`);
|
|
391
391
|
strategiesUsed.push(match.strategy);
|
|
392
392
|
next = require_editStrategies.applyEdit(next, match, edit.newText);
|
|
393
393
|
}
|