diff options
| author | Adam Malczewski <[email protected]> | 2026-06-05 21:20:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-05 21:20:12 +0900 |
| commit | 7fb3269c698ae583ea7997ce206c4ae252fd3218 (patch) | |
| tree | 247d03408ecccd633290ea56b1b08811ebe460ec /packages/tool-read-file/src | |
| parent | 4283d1f8a0bc3953e65962a2364c903d0015f047 (diff) | |
| download | dispatch-7fb3269c698ae583ea7997ce206c4ae252fd3218.tar.gz dispatch-7fb3269c698ae583ea7997ce206c4ae252fd3218.zip | |
feat(backend): credential-store + model selection/catalog (GET /models) + per-turn cwd through orchestrator/transport/host-bin
Diffstat (limited to 'packages/tool-read-file/src')
| -rw-r--r-- | packages/tool-read-file/src/read-file.test.ts | 68 | ||||
| -rw-r--r-- | packages/tool-read-file/src/read-file.ts | 21 |
2 files changed, 79 insertions, 10 deletions
diff --git a/packages/tool-read-file/src/read-file.test.ts b/packages/tool-read-file/src/read-file.test.ts index f995b09..2725a05 100644 --- a/packages/tool-read-file/src/read-file.test.ts +++ b/packages/tool-read-file/src/read-file.test.ts @@ -11,7 +11,7 @@ import { validateArgs, } from "./read-file.js"; -function stubCtx(): ToolExecuteContext { +function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext { return { toolCallId: "test-call-1", onOutput: () => {}, @@ -21,6 +21,7 @@ function stubCtx(): ToolExecuteContext { { emit: () => {} }, { now: () => 0, newId: () => "id" }, ), + ...overrides, }; } @@ -250,4 +251,69 @@ describe("createReadFileTool", () => { expect(tool.parameters.required).toEqual(["path"]); expect(tool.parameters.properties?.path?.type).toBe("string"); }); + + it("reads file under ctx.cwd when set (not baked workdir)", async () => { + const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-")); + try { + const filePath = join(ctxDir, "ctx-file.txt"); + await writeFile(filePath, "from ctx cwd", "utf8"); + + const tool = createReadFileTool(workdir); // baked workdir is different + const result = await tool.execute({ path: "ctx-file.txt" }, stubCtx({ cwd: ctxDir })); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: from ctx cwd"); + } finally { + await rm(ctxDir, { recursive: true, force: true }); + } + }); + + it("rejects path escaping ctx.cwd via ..", async () => { + const ctxDir = await mkdtemp(join(tmpdir(), "ctx-escape-test-")); + try { + const tool = createReadFileTool(workdir); + const result = await tool.execute({ path: "../escape.txt" }, stubCtx({ cwd: ctxDir })); + + expect(result.isError).toBe(true); + expect(result.content).toContain("outside the working directory"); + } finally { + await rm(ctxDir, { recursive: true, force: true }); + } + }); + + it("rejects symlink escaping ctx.cwd", async () => { + const ctxDir = await mkdtemp(join(tmpdir(), "ctx-symlink-test-")); + const outsideDir = await mkdtemp(join(tmpdir(), "ctx-outside-")); + try { + const outsideFile = join(outsideDir, "secret.txt"); + await writeFile(outsideFile, "secret data", "utf8"); + + const symlinkPath = join(ctxDir, "link.txt"); + const { symlink } = await import("node:fs/promises"); + await symlink(outsideFile, symlinkPath); + + const tool = createReadFileTool(workdir); + const result = await tool.execute({ path: "link.txt" }, stubCtx({ cwd: ctxDir })); + + expect(result.isError).toBe(true); + expect(result.content).toContain("outside the working directory"); + } finally { + await rm(ctxDir, { recursive: true, force: true }); + await rm(outsideDir, { recursive: true, force: true }); + } + }); + + it("falls back to baked workdir when ctx.cwd is omitted", async () => { + const filePath = join(workdir, "baked-file.txt"); + await writeFile(filePath, "from baked workdir", "utf8"); + + const tool = createReadFileTool(workdir); + const ctx = stubCtx(); + // Ensure cwd is undefined + expect(ctx.cwd).toBeUndefined(); + const result = await tool.execute({ path: "baked-file.txt" }, ctx); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: from baked workdir"); + }); }); diff --git a/packages/tool-read-file/src/read-file.ts b/packages/tool-read-file/src/read-file.ts index b5bb0f1..d4a4de8 100644 --- a/packages/tool-read-file/src/read-file.ts +++ b/packages/tool-read-file/src/read-file.ts @@ -103,7 +103,7 @@ export function createReadFileTool(workingDirectory: string): ToolContract { required: ["path"], }, concurrencySafe: true, - async execute(args: unknown, _ctx): Promise<ToolResult> { + async execute(args: unknown, ctx): Promise<ToolResult> { const validated = validateArgs(args); if ("error" in validated) { return { content: validated.error, isError: true }; @@ -111,11 +111,14 @@ export function createReadFileTool(workingDirectory: string): ToolContract { const { path: relPath, offset, limit } = validated; - // Resolve the requested path against the working directory. - const resolvedPath = resolve(workdir, relPath); + // Effective base: per-turn ctx.cwd overrides the baked workdir. + const effectiveBase = ctx.cwd ? resolve(ctx.cwd) : workdir; - // Basic prefix check (catches ".." and absolute paths outside workdir). - if (!isPathWithinWorkdir(resolvedPath, workdir)) { + // Resolve the requested path against the effective base. + const resolvedPath = resolve(effectiveBase, relPath); + + // Basic prefix check (catches ".." and absolute paths outside effectiveBase). + if (!isPathWithinWorkdir(resolvedPath, effectiveBase)) { return { content: `Error: Path "${relPath}" is outside the working directory.`, isError: true, @@ -124,11 +127,11 @@ export function createReadFileTool(workingDirectory: string): ToolContract { // Symlink hardening: realpath both and re-check containment. let realResolved: string; - let realWorkdir: string; + let realBase: string; try { - [realResolved, realWorkdir] = await Promise.all([ + [realResolved, realBase] = await Promise.all([ realpath(resolvedPath), - realpath(workdir), + realpath(effectiveBase), ]); } catch (err: unknown) { const code = (err as NodeJS.ErrnoException).code; @@ -141,7 +144,7 @@ export function createReadFileTool(workingDirectory: string): ToolContract { }; } - if (!isPathWithinWorkdir(realResolved, realWorkdir)) { + if (!isPathWithinWorkdir(realResolved, realBase)) { return { content: `Error: Path "${relPath}" is outside the working directory.`, isError: true, |
