File size: 11,540 Bytes
017c628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff2afe2
 
 
017c628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff2afe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
017c628
 
 
 
 
 
 
 
 
 
 
 
 
 
2a70ee5
017c628
 
 
ff2afe2
 
017c628
 
 
 
 
ff2afe2
 
017c628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a70ee5
017c628
 
 
ff2afe2
 
017c628
 
 
 
 
ff2afe2
 
017c628
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ff2afe2
017c628
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
];