openclaw/src/agents/pi-tools.workspace-only-false.test.ts
Peter Steinberger 65e13c7b6e fix(fs): honor unset tools.fs.workspaceOnly default (land #31128 by @SaucePackets)
Landed-from: #31128
Contributor: @SaucePackets
Co-authored-by: SaucePackets <33006469+SaucePackets@users.noreply.github.com>
2026-03-02 01:43:50 +00:00

248 lines
7.3 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createOpenClawCodingTools } from "./pi-tools.js";
describe("FS tools with workspaceOnly=false", () => {
let tmpDir: string;
let workspaceDir: string;
let outsideFile: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
workspaceDir = path.join(tmpDir, "workspace");
await fs.mkdir(workspaceDir);
outsideFile = path.join(tmpDir, "outside.txt");
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
it("should allow write outside workspace when workspaceOnly=false", async () => {
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: false,
},
},
},
});
const writeTool = tools.find((t) => t.name === "write");
expect(writeTool).toBeDefined();
const result = await writeTool!.execute("test-call-1", {
path: outsideFile,
content: "test content",
});
// Check if the operation succeeded (no error in content)
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideFile, "utf-8");
expect(content).toBe("test content");
});
it("should allow write outside workspace via ../ path when workspaceOnly=false", async () => {
const relativeOutsidePath = path.join("..", "outside-relative-write.txt");
const outsideRelativeFile = path.join(tmpDir, "outside-relative-write.txt");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: false,
},
},
},
});
const writeTool = tools.find((t) => t.name === "write");
expect(writeTool).toBeDefined();
const result = await writeTool!.execute("test-call-1b", {
path: relativeOutsidePath,
content: "relative test content",
});
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideRelativeFile, "utf-8");
expect(content).toBe("relative test content");
});
it("should allow edit outside workspace when workspaceOnly=false", async () => {
await fs.writeFile(outsideFile, "old content");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: false,
},
},
},
});
const editTool = tools.find((t) => t.name === "edit");
expect(editTool).toBeDefined();
const result = await editTool!.execute("test-call-2", {
path: outsideFile,
oldText: "old content",
newText: "new content",
});
// Check if the operation succeeded (no error in content)
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideFile, "utf-8");
expect(content).toBe("new content");
});
it("should allow edit outside workspace via ../ path when workspaceOnly=false", async () => {
const relativeOutsidePath = path.join("..", "outside-relative-edit.txt");
const outsideRelativeFile = path.join(tmpDir, "outside-relative-edit.txt");
await fs.writeFile(outsideRelativeFile, "old relative content");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: false,
},
},
},
});
const editTool = tools.find((t) => t.name === "edit");
expect(editTool).toBeDefined();
const result = await editTool!.execute("test-call-2b", {
path: relativeOutsidePath,
oldText: "old relative content",
newText: "new relative content",
});
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideRelativeFile, "utf-8");
expect(content).toBe("new relative content");
});
it("should allow read outside workspace when workspaceOnly=false", async () => {
await fs.writeFile(outsideFile, "test read content");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: false,
},
},
},
});
const readTool = tools.find((t) => t.name === "read");
expect(readTool).toBeDefined();
const result = await readTool!.execute("test-call-3", {
path: outsideFile,
});
// Check if the operation succeeded (no error in content)
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
});
it("should allow write outside workspace when workspaceOnly is unset", async () => {
const outsideUnsetFile = path.join(tmpDir, "outside-unset-write.txt");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {},
});
const writeTool = tools.find((t) => t.name === "write");
expect(writeTool).toBeDefined();
const result = await writeTool!.execute("test-call-3a", {
path: outsideUnsetFile,
content: "unset write content",
});
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideUnsetFile, "utf-8");
expect(content).toBe("unset write content");
});
it("should allow edit outside workspace when workspaceOnly is unset", async () => {
const outsideUnsetFile = path.join(tmpDir, "outside-unset-edit.txt");
await fs.writeFile(outsideUnsetFile, "before");
const tools = createOpenClawCodingTools({
workspaceDir,
config: {},
});
const editTool = tools.find((t) => t.name === "edit");
expect(editTool).toBeDefined();
const result = await editTool!.execute("test-call-3b", {
path: outsideUnsetFile,
oldText: "before",
newText: "after",
});
const hasError = result.content.some(
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
);
expect(hasError).toBe(false);
const content = await fs.readFile(outsideUnsetFile, "utf-8");
expect(content).toBe("after");
});
it("should block write outside workspace when workspaceOnly=true", async () => {
const tools = createOpenClawCodingTools({
workspaceDir,
config: {
tools: {
fs: {
workspaceOnly: true,
},
},
},
});
const writeTool = tools.find((t) => t.name === "write");
expect(writeTool).toBeDefined();
// When workspaceOnly=true, the guard throws an error
await expect(
writeTool!.execute("test-call-4", {
path: outsideFile,
content: "test content",
}),
).rejects.toThrow(/Path escapes (workspace|sandbox) root/);
});
});