summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-write-file/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 23:32:23 +0900
committerAdam Malczewski <[email protected]>2026-06-21 23:32:23 +0900
commit7f6fd218ceeb3a1cf9420f5f6cfa4d70da6987bb (patch)
treee706f49d2df591c468fb66febe2a3f29d23cb0bb /packages/tool-write-file/src
parent62ea07f56ff066bbf05041aadf8006cbc65e5c53 (diff)
downloaddispatch-7f6fd218ceeb3a1cf9420f5f6cfa4d70da6987bb.tar.gz
dispatch-7f6fd218ceeb3a1cf9420f5f6cfa4d70da6987bb.zip
feat: remove CWD path containment from file tools
read_file, write_file, and edit_file no longer restrict access to paths outside the working directory. The isPathWithinWorkdir prefix check and symlink hardening have been removed from all three tools. This allows agents to read and write files anywhere on the filesystem, not just within the per-turn cwd. The shell tool already had no such restriction.
Diffstat (limited to 'packages/tool-write-file/src')
-rw-r--r--packages/tool-write-file/src/index.ts1
-rw-r--r--packages/tool-write-file/src/write-file.test.ts60
-rw-r--r--packages/tool-write-file/src/write-file.ts68
3 files changed, 4 insertions, 125 deletions
diff --git a/packages/tool-write-file/src/index.ts b/packages/tool-write-file/src/index.ts
index 521a429..8a2cb36 100644
--- a/packages/tool-write-file/src/index.ts
+++ b/packages/tool-write-file/src/index.ts
@@ -2,6 +2,5 @@ export { extension } from "./extension.js";
export {
createWriteFileTool,
decideOverwrite,
- isPathWithinWorkdir,
validateArgs,
} from "./write-file.js";
diff --git a/packages/tool-write-file/src/write-file.test.ts b/packages/tool-write-file/src/write-file.test.ts
index cf4fa64..6b316bc 100644
--- a/packages/tool-write-file/src/write-file.test.ts
+++ b/packages/tool-write-file/src/write-file.test.ts
@@ -3,12 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { createLogger, type ToolExecuteContext } from "@dispatch/kernel";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
-import {
- createWriteFileTool,
- decideOverwrite,
- isPathWithinWorkdir,
- validateArgs,
-} from "./write-file.js";
+import { createWriteFileTool, decideOverwrite, validateArgs } from "./write-file.js";
function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext {
return {
@@ -75,24 +70,6 @@ describe("decideOverwrite", () => {
});
});
-describe("isPathWithinWorkdir", () => {
- it("accepts a path within workdir", () => {
- expect(isPathWithinWorkdir("/tmp/workdir/file.txt", "/tmp/workdir")).toBe(true);
- });
-
- it("accepts the workdir itself", () => {
- expect(isPathWithinWorkdir("/tmp/workdir", "/tmp/workdir")).toBe(true);
- });
-
- it("rejects a path outside workdir", () => {
- expect(isPathWithinWorkdir("/tmp/other/file.txt", "/tmp/workdir")).toBe(false);
- });
-
- it("rejects a prefix attack (workdir prefix but different dir)", () => {
- expect(isPathWithinWorkdir("/tmp/workdir-evil/file.txt", "/tmp/workdir")).toBe(false);
- });
-});
-
describe("validateArgs", () => {
it("returns validated args for valid input", () => {
const result = validateArgs({ path: "foo.txt", content: "hello" });
@@ -192,23 +169,7 @@ describe("createWriteFileTool", () => {
const result = await tool.execute({ path: "no/such/dir/file.txt", content: "data" }, stubCtx());
expect(result.isError).toBe(true);
- expect(result.content).toContain("Parent directory");
- });
-
- it("rejects a path outside the working directory", async () => {
- const tool = createWriteFileTool(workdir);
- const result = await tool.execute({ path: "../escape.txt", content: "data" }, stubCtx());
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("outside the working directory");
- });
-
- it("rejects an absolute path outside workdir", async () => {
- const tool = createWriteFileTool(workdir);
- const result = await tool.execute({ path: "/tmp/escape.txt", content: "data" }, stubCtx());
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("outside the working directory");
+ expect(result.content).toContain("Error");
});
it("concurrencySafe is false", () => {
@@ -253,23 +214,6 @@ describe("createWriteFileTool", () => {
}
});
- it("handles symlink escape attempt", async () => {
- const outsideDir = await mkdtemp(join(tmpdir(), "outside-"));
- try {
- const symlinkPath = join(workdir, "link.txt");
- const { symlink } = await import("node:fs/promises");
- await symlink(join(outsideDir, "target.txt"), symlinkPath);
-
- const tool = createWriteFileTool(workdir);
- const result = await tool.execute({ path: "link.txt", content: "escape" }, stubCtx());
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("outside the working directory");
- } finally {
- await rm(outsideDir, { recursive: true, force: true });
- }
- });
-
it("writes empty content", async () => {
const tool = createWriteFileTool(workdir);
const result = await tool.execute({ path: "empty.txt", content: "" }, stubCtx());
diff --git a/packages/tool-write-file/src/write-file.ts b/packages/tool-write-file/src/write-file.ts
index 16cfc83..1317ce8 100644
--- a/packages/tool-write-file/src/write-file.ts
+++ b/packages/tool-write-file/src/write-file.ts
@@ -1,5 +1,5 @@
-import { access, lstat, readlink, realpath, stat, writeFile } from "node:fs/promises";
-import { dirname, resolve, sep } from "node:path";
+import { access, stat, writeFile } from "node:fs/promises";
+import { resolve } from "node:path";
import type { ToolContract, ToolResult } from "@dispatch/kernel";
interface ValidatedArgs {
@@ -20,12 +20,6 @@ export function decideOverwrite(fileExists: boolean, overwrite: boolean): Overwr
return { error: "Error: overwrite: true but the file does not exist." };
}
-/** Pure: check that a resolved absolute path is within the workdir (prefix check). */
-export function isPathWithinWorkdir(resolvedPath: string, workdir: string): boolean {
- const normalizedWorkdir = workdir.endsWith(sep) ? workdir : workdir + sep;
- return resolvedPath === workdir || resolvedPath.startsWith(normalizedWorkdir);
-}
-
/** Pure: validate and coerce args from the model. */
export function validateArgs(args: unknown): ValidatedArgs | { readonly error: string } {
if (args === null || args === undefined || typeof args !== "object") {
@@ -103,64 +97,6 @@ export function createWriteFileTool(workingDirectory: string): ToolContract {
const effectiveBase = ctx.cwd ? resolve(ctx.cwd) : workdir;
const resolvedPath = resolve(effectiveBase, relPath);
- if (!isPathWithinWorkdir(resolvedPath, effectiveBase)) {
- return {
- content: `Error: Path "${relPath}" is outside the working directory.`,
- isError: true,
- };
- }
-
- // Symlink hardening: realpath the parent directory and the base, then re-check.
- let realParent: string;
- let realBase: string;
- try {
- const parentDir = dirname(resolvedPath);
- [realParent, realBase] = await Promise.all([realpath(parentDir), realpath(effectiveBase)]);
- } catch (err: unknown) {
- const code = (err as NodeJS.ErrnoException).code;
- if (code === "ENOENT") {
- return {
- content: `Error: Parent directory for "${relPath}" does not exist.`,
- isError: true,
- };
- }
- return {
- content: `Error resolving path: ${err instanceof Error ? err.message : String(err)}`,
- isError: true,
- };
- }
-
- const realResolvedPath = realParent + sep + resolvedPath.split(sep).at(-1);
- if (!isPathWithinWorkdir(realResolvedPath, realBase)) {
- return {
- content: `Error: Path "${relPath}" is outside the working directory.`,
- isError: true,
- };
- }
-
- // If the resolved path itself is a symlink, verify the target is contained.
- try {
- const linkStat = await lstat(resolvedPath);
- if (linkStat.isSymbolicLink()) {
- const linkTarget = await readlink(resolvedPath);
- const resolvedTarget = resolve(dirname(resolvedPath), linkTarget);
- if (!isPathWithinWorkdir(resolvedTarget, realBase)) {
- return {
- content: `Error: Path "${relPath}" is outside the working directory.`,
- isError: true,
- };
- }
- }
- } catch (err: unknown) {
- const code = (err as NodeJS.ErrnoException).code;
- if (code !== "ENOENT") {
- return {
- content: `Error checking path: ${err instanceof Error ? err.message : String(err)}`,
- isError: true,
- };
- }
- }
-
// Check existence.
let fileExists = false;
try {