diff --git a/mcp/src/repo/index.ts b/mcp/src/repo/index.ts index 3b1df9ead..693da3081 100644 --- a/mcp/src/repo/index.ts +++ b/mcp/src/repo/index.ts @@ -1,6 +1,7 @@ import { cloneOrUpdateRepo } from "./clone.js"; import { get_context, stream_context } from "./agent.js"; -import { ToolsConfig, SkillsConfig, GgnnConfig, getDefaultToolDescriptions, normalizeToolsConfig } from "./tools.js"; +import { ToolsConfig, SkillsConfig, GgnnConfig, getDefaultToolDescriptions, normalizeToolsConfig, editorRoots } from "./tools.js"; +import { resolveInCwd } from "./textEdit.js"; import { type SubAgent, normalizeSubAgent } from "./subagent.js"; import { Request, Response } from "express"; import { ModelMessage } from "ai"; @@ -606,10 +607,21 @@ export async function get_agent_file(req: Request, res: Response) { return; } - if (!existsSync(filePath)) { + // Confine reads to the same sandbox the editor tool writes into (cloned repos + // under /tmp, the OS scratch dir, and the durable artifacts dir). Refuses + // absolute paths and ../ traversal that escape those roots. + let resolved: string; + try { + resolved = resolveInCwd(filePath, editorRoots("/tmp")); + } catch { + res.status(403).json({ error: "Path outside allowed roots" }); + return; + } + + if (!existsSync(resolved)) { res.status(404).json({ error: "File not found" }); return; } - res.sendFile(path.resolve(filePath)); + res.sendFile(resolved); } diff --git a/mcp/src/repo/tools.ts b/mcp/src/repo/tools.ts index c8902f35e..a46d17dc2 100644 --- a/mcp/src/repo/tools.ts +++ b/mcp/src/repo/tools.ts @@ -30,7 +30,7 @@ import { relevant_node_types } from "../graph/types.js"; * path outside these (see resolveInCwd in textEdit.ts): the cloned repo, the * OS temp dir (scratch), and — when configured — the durable artifacts dir. */ -function editorRoots(repoPath: string): string[] { +export function editorRoots(repoPath: string): string[] { const roots = [repoPath, os.tmpdir()]; if (AGENT_ARTIFACTS_DIR) roots.push(AGENT_ARTIFACTS_DIR); return roots;