Spaces:
Runtime error
Runtime error
| import { tool } from "@langchain/core/tools"; | |
| import { z } from "zod"; | |
| import fs from "fs/promises"; | |
| import path from "path"; | |
| import { exec } from "child_process"; | |
| import { promisify } from "util"; | |
| const execAsync = promisify(exec); | |
| // --------------------------------------------------------------------------- | |
| // Exploration Tools (Basic Tools) | |
| // --------------------------------------------------------------------------- | |
| export const readFileTool = tool( | |
| async ({ filePath }, config) => { | |
| try { | |
| // The sandboxDir is passed in via the config.configurable object | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const absolutePath = path.resolve(sandboxDir, filePath); | |
| // Prevent directory traversal outside sandbox | |
| if (!absolutePath.startsWith(path.resolve(sandboxDir))) { | |
| return "Error: Access denied. Cannot read files outside the project sandbox."; | |
| } | |
| const content = await fs.readFile(absolutePath, "utf-8"); | |
| return content; | |
| } catch (e: any) { | |
| return `Error reading file: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "read_file", | |
| description: "Reads the contents of a specific file in the project.", | |
| schema: z.object({ | |
| filePath: z.string().describe("The relative path to the file to read (e.g. 'src/Vault.sol')"), | |
| }), | |
| } | |
| ); | |
| export const listDirTool = tool( | |
| async ({ dirPath }, config) => { | |
| try { | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const absolutePath = path.resolve(sandboxDir, dirPath || "."); | |
| if (!absolutePath.startsWith(path.resolve(sandboxDir))) { | |
| return "Error: Access denied. Cannot list directories outside the project sandbox."; | |
| } | |
| const files = await fs.readdir(absolutePath, { withFileTypes: true }); | |
| return files.map(f => `${f.isDirectory() ? '[DIR]' : '[FILE]'} ${f.name}`).join("\n"); | |
| } catch (e: any) { | |
| return `Error listing directory: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "list_dir", | |
| description: "Lists files and directories in a given path to understand project structure.", | |
| schema: z.object({ | |
| dirPath: z.string().optional().describe("The relative path to the directory (e.g. 'src' or 'test/mocks'). Defaults to root."), | |
| }), | |
| } | |
| ); | |
| export const grepSearchTool = tool( | |
| async ({ query, dirPath }, config) => { | |
| try { | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const targetDir = path.resolve(sandboxDir, dirPath || "."); | |
| // Use grep -rnw to search recursively | |
| // Note: In a real production system, use a safe regex/grep library or escape properly. | |
| const cmd = `grep -rn "${query.replace(/"/g, '\\"')}" ${targetDir} | head -n 50`; | |
| const { stdout } = await execAsync(cmd); | |
| return stdout || "No matches found."; | |
| } catch (e: any) { | |
| // grep returns exit code 1 if no matches are found | |
| if (e.code === 1) return "No matches found."; | |
| return `Error executing search: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "grep_search", | |
| description: "Searches the codebase recursively for specific symbols, variable names, or interfaces.", | |
| schema: z.object({ | |
| query: z.string().describe("The text or symbol to search for (e.g. 'interface IERC20' or 'withdraw(')"), | |
| dirPath: z.string().optional().describe("The relative directory to search in (e.g. 'src'). Defaults to root."), | |
| }), | |
| } | |
| ); | |
| // --------------------------------------------------------------------------- | |
| // Modification Tools (File Editing) | |
| // --------------------------------------------------------------------------- | |
| export const writeFileTool = tool( | |
| async ({ filePath, content }, config) => { | |
| try { | |
| if (filePath === "foundry.toml" || filePath === "remappings.txt" || filePath.endsWith(".gitmodules")) { | |
| return "Error: You are strictly forbidden from modifying foundry.toml, remappings.txt, or .gitmodules. The environment is already perfectly configured."; | |
| } | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const absolutePath = path.resolve(sandboxDir, filePath); | |
| if (!absolutePath.startsWith(path.resolve(sandboxDir))) { | |
| return "Error: Access denied. Cannot write files outside the project sandbox."; | |
| } | |
| // Ensure directory exists | |
| await fs.mkdir(path.dirname(absolutePath), { recursive: true }); | |
| await fs.writeFile(absolutePath, content, "utf-8"); | |
| return `Successfully wrote to ${filePath}`; | |
| } catch (e: any) { | |
| return `Error writing file: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "write_file", | |
| description: "Writes or overwrites a file with the provided content. Primarily used to write 'test/Exploit.t.sol'.", | |
| schema: z.object({ | |
| filePath: z.string().describe("The relative path to write to (e.g. 'test/Exploit.t.sol')"), | |
| content: z.string().describe("The full content of the file to write."), | |
| }), | |
| } | |
| ); | |
| export const editFileTool = tool( | |
| async ({ filePath, searchString, replacementString }, config) => { | |
| try { | |
| if (filePath === "foundry.toml" || filePath === "remappings.txt" || filePath.endsWith(".gitmodules")) { | |
| return "Error: You are strictly forbidden from modifying foundry.toml, remappings.txt, or .gitmodules. The environment is already perfectly configured."; | |
| } | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const absolutePath = path.resolve(sandboxDir, filePath); | |
| if (!absolutePath.startsWith(path.resolve(sandboxDir))) { | |
| return "Error: Access denied. Cannot edit files outside the project sandbox."; | |
| } | |
| const content = await fs.readFile(absolutePath, "utf-8"); | |
| if (!content.includes(searchString)) { | |
| return "Error: searchString not found in the file. Ensure you pass the exact string to be replaced."; | |
| } | |
| // We only replace the first occurrence or all? Replacing all is safer if they match exactly. | |
| // But standard string replace only replaces the first occurrence, which is safer if multiple matches exist. | |
| const newContent = content.replace(searchString, replacementString); | |
| if (newContent === content) { | |
| return "Error: replacement resulted in no changes."; | |
| } | |
| await fs.writeFile(absolutePath, newContent, "utf-8"); | |
| return `Successfully edited ${filePath}`; | |
| } catch (e: any) { | |
| return `Error editing file: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "edit_file", | |
| description: "Edits an existing file by replacing a specific block of text. Use this instead of write_file for small changes.", | |
| schema: z.object({ | |
| filePath: z.string().describe("The relative path to edit (e.g. 'test/Exploit.t.sol')"), | |
| searchString: z.string().describe("The exact text block to search for and replace. Must match perfectly including whitespace."), | |
| replacementString: z.string().describe("The new text block to insert in place of searchString."), | |
| }), | |
| } | |
| ); | |
| // --------------------------------------------------------------------------- | |
| // Smart Contract Tools (Execution Feedback) | |
| // --------------------------------------------------------------------------- | |
| export const smartContractCompileTool = tool( | |
| async (_, config) => { | |
| try { | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const { stdout, stderr } = await execAsync( | |
| "forge build", | |
| { | |
| cwd: sandboxDir, | |
| timeout: 30000, | |
| env: { ...process.env } | |
| } | |
| ); | |
| const out = stdout ? String(stdout).slice(-4000) : ""; | |
| const errOut = stderr ? String(stderr).slice(-4000) : ""; | |
| return `Compilation Successful:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`; | |
| } catch (err: any) { | |
| if (err.killed || err.signal === "SIGTERM") { | |
| return "Error: Compilation timed out after 30s."; | |
| } | |
| const out = err.stdout ? String(err.stdout).slice(-4000) : ""; | |
| const errOut = err.stderr ? String(err.stderr).slice(-4000) : ""; | |
| return `Compilation Failed:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`; | |
| } | |
| }, | |
| { | |
| name: "smart_contract_compile", | |
| description: "Runs 'forge build' to compile the smart contracts and tests. Returns stdout and stderr. Use this to check for syntax errors before testing.", | |
| schema: z.object({}), | |
| } | |
| ); | |
| export const smartContractTestTool = tool( | |
| async ({ testMatch }, config) => { | |
| try { | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const matchArg = testMatch ? `--match-contract ${testMatch}` : ""; | |
| const { stdout, stderr } = await execAsync( | |
| `forge test ${matchArg} -vvvv`, | |
| { | |
| cwd: sandboxDir, | |
| timeout: 60000, | |
| env: { ...process.env } | |
| } | |
| ); | |
| const out = stdout ? String(stdout).slice(-4000) : ""; | |
| const errOut = stderr ? String(stderr).slice(-4000) : ""; | |
| return `Test Passed Successfully!\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`; | |
| } catch (err: any) { | |
| if (err.killed || err.signal === "SIGTERM") { | |
| return "Error: Test execution timed out after 60s."; | |
| } | |
| const out = err.stdout ? String(err.stdout).slice(-4000) : ""; | |
| const errOut = err.stderr ? String(err.stderr).slice(-4000) : ""; | |
| return `Test Failed:\nSTDOUT:\n${out}\nSTDERR:\n${errOut}`; | |
| } | |
| }, | |
| { | |
| name: "smart_contract_test", | |
| description: "Runs 'forge test -vvvv' to execute the PoC exploit. Returns the execution traces and assertions. Crucial for verifying if the exploit works or why it reverted.", | |
| schema: z.object({ | |
| testMatch: z.string().optional().describe("Optional test contract name to match (e.g. 'ExploitTest')"), | |
| }), | |
| } | |
| ); | |
| // --------------------------------------------------------------------------- | |
| // Planning Tool | |
| // --------------------------------------------------------------------------- | |
| export const todoPlannerTool = tool( | |
| async ({ action, task }, config) => { | |
| try { | |
| const sandboxDir = config?.configurable?.sandboxDir || process.cwd(); | |
| const todoPath = path.resolve(sandboxDir, "todo_plan.txt"); | |
| if (action === "read") { | |
| try { | |
| return await fs.readFile(todoPath, "utf-8"); | |
| } catch { | |
| return "No tasks found. Todo list is empty."; | |
| } | |
| } | |
| if (action === "add" && task) { | |
| await fs.appendFile(todoPath, `- [ ] ${task}\n`); | |
| return `Added task: ${task}`; | |
| } | |
| if (action === "update" && task) { | |
| // Overwrite with the full new state provided by the LLM | |
| await fs.writeFile(todoPath, task); | |
| return "Todo list updated."; | |
| } | |
| return "Invalid action."; | |
| } catch (e: any) { | |
| return `Error with planner: ${e.message}`; | |
| } | |
| }, | |
| { | |
| name: "todo_planner", | |
| description: "A lightweight planning utility to organize tasks. Actions: 'read' to view tasks, 'add' to append a task, 'update' to overwrite the whole list with new state.", | |
| schema: z.object({ | |
| action: z.enum(["read", "add", "update"]).describe("The action to perform."), | |
| task: z.string().optional().describe("The task text to add, or the full new list to update."), | |
| }), | |
| } | |
| ); | |
| export const pocoTools = [ | |
| readFileTool, | |
| listDirTool, | |
| grepSearchTool, | |
| writeFileTool, | |
| editFileTool, | |
| smartContractCompileTool, | |
| smartContractTestTool, | |
| todoPlannerTool | |
| ]; | |