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>
This commit is contained in:
Peter Steinberger
2026-03-02 01:43:50 +00:00
parent f1354869bd
commit 65e13c7b6e
3 changed files with 53 additions and 4 deletions

View File

@@ -763,7 +763,7 @@ function createSandboxEditOperations(params: SandboxToolParams) {
}
function createHostWriteOperations(root: string, options?: { workspaceOnly?: boolean }) {
const workspaceOnly = options?.workspaceOnly !== false;
const workspaceOnly = options?.workspaceOnly ?? false;
if (!workspaceOnly) {
// When workspaceOnly is false, allow writes anywhere on the host
@@ -781,7 +781,7 @@ function createHostWriteOperations(root: string, options?: { workspaceOnly?: boo
} as const;
}
// When workspaceOnly is true (default), enforce workspace boundary
// When workspaceOnly is true, enforce workspace boundary
return {
mkdir: async (dir: string) => {
const relative = toRelativePathInRoot(root, dir, { allowRoot: true });
@@ -802,7 +802,7 @@ function createHostWriteOperations(root: string, options?: { workspaceOnly?: boo
}
function createHostEditOperations(root: string, options?: { workspaceOnly?: boolean }) {
const workspaceOnly = options?.workspaceOnly !== false;
const workspaceOnly = options?.workspaceOnly ?? false;
if (!workspaceOnly) {
// When workspaceOnly is false, allow edits anywhere on the host
@@ -824,7 +824,7 @@ function createHostEditOperations(root: string, options?: { workspaceOnly?: bool
} as const;
}
// When workspaceOnly is true (default), enforce workspace boundary
// When workspaceOnly is true, enforce workspace boundary
return {
readFile: async (absolutePath: string) => {
const relative = toRelativePathInRoot(root, absolutePath);

View File

@@ -173,6 +173,54 @@ describe("FS tools with workspaceOnly=false", () => {
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,