mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 09:31:41 +00:00
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:
@@ -102,6 +102,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Agents/FS workspace default: honor documented host file-tool default `tools.fs.workspaceOnly=false` when unset so host `write`/`edit` calls are not incorrectly workspace-restricted unless explicitly enabled. Landed from contributor PR #31128 by @SaucePackets. Thanks @SaucePackets.
|
||||||
- Gateway/CLI session recovery: handle expired CLI session IDs gracefully by clearing stale session state and retrying without crashing gateway runs. Landed from contributor PR #31090 by @frankekn. Thanks @frankekn.
|
- Gateway/CLI session recovery: handle expired CLI session IDs gracefully by clearing stale session state and retrying without crashing gateway runs. Landed from contributor PR #31090 by @frankekn. Thanks @frankekn.
|
||||||
- Slack/Subagent completion delivery: stop forcing bound conversation IDs into `threadId` so Slack completion announces do not send invalid `thread_ts` for DMs/top-level channels. Landed from contributor PR #31105 by @stakeswky. Thanks @stakeswky.
|
- Slack/Subagent completion delivery: stop forcing bound conversation IDs into `threadId` so Slack completion announces do not send invalid `thread_ts` for DMs/top-level channels. Landed from contributor PR #31105 by @stakeswky. Thanks @stakeswky.
|
||||||
- Signal/Loop protection: evaluate own-account detection before sync-message filtering (including UUID-only `accountUuid` configs) so `sentTranscript` sync events cannot bypass loop protection and self-reply loops. Landed from contributor PR #31093 by @kevinWangSheng. Thanks @kevinWangSheng.
|
- Signal/Loop protection: evaluate own-account detection before sync-message filtering (including UUID-only `accountUuid` configs) so `sentTranscript` sync events cannot bypass loop protection and self-reply loops. Landed from contributor PR #31093 by @kevinWangSheng. Thanks @kevinWangSheng.
|
||||||
|
|||||||
@@ -763,7 +763,7 @@ function createSandboxEditOperations(params: SandboxToolParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createHostWriteOperations(root: string, options?: { workspaceOnly?: boolean }) {
|
function createHostWriteOperations(root: string, options?: { workspaceOnly?: boolean }) {
|
||||||
const workspaceOnly = options?.workspaceOnly !== false;
|
const workspaceOnly = options?.workspaceOnly ?? false;
|
||||||
|
|
||||||
if (!workspaceOnly) {
|
if (!workspaceOnly) {
|
||||||
// When workspaceOnly is false, allow writes anywhere on the host
|
// When workspaceOnly is false, allow writes anywhere on the host
|
||||||
@@ -781,7 +781,7 @@ function createHostWriteOperations(root: string, options?: { workspaceOnly?: boo
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When workspaceOnly is true (default), enforce workspace boundary
|
// When workspaceOnly is true, enforce workspace boundary
|
||||||
return {
|
return {
|
||||||
mkdir: async (dir: string) => {
|
mkdir: async (dir: string) => {
|
||||||
const relative = toRelativePathInRoot(root, dir, { allowRoot: true });
|
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 }) {
|
function createHostEditOperations(root: string, options?: { workspaceOnly?: boolean }) {
|
||||||
const workspaceOnly = options?.workspaceOnly !== false;
|
const workspaceOnly = options?.workspaceOnly ?? false;
|
||||||
|
|
||||||
if (!workspaceOnly) {
|
if (!workspaceOnly) {
|
||||||
// When workspaceOnly is false, allow edits anywhere on the host
|
// When workspaceOnly is false, allow edits anywhere on the host
|
||||||
@@ -824,7 +824,7 @@ function createHostEditOperations(root: string, options?: { workspaceOnly?: bool
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When workspaceOnly is true (default), enforce workspace boundary
|
// When workspaceOnly is true, enforce workspace boundary
|
||||||
return {
|
return {
|
||||||
readFile: async (absolutePath: string) => {
|
readFile: async (absolutePath: string) => {
|
||||||
const relative = toRelativePathInRoot(root, absolutePath);
|
const relative = toRelativePathInRoot(root, absolutePath);
|
||||||
|
|||||||
@@ -173,6 +173,54 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
expect(hasError).toBe(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 () => {
|
it("should block write outside workspace when workspaceOnly=true", async () => {
|
||||||
const tools = createOpenClawCodingTools({
|
const tools = createOpenClawCodingTools({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user