From da80e22d89d11f00e80012381071555365e91cbe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 01:00:49 +0000 Subject: [PATCH] fix(tools): land #31015 from @haosenwang1018 Co-authored-by: haosenwang1018 <1293965075@qq.com> --- CHANGELOG.md | 1 + .../pi-tools.read.host-edit-access.test.ts | 8 ++++++-- src/agents/pi-tools.read.ts | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2520bf6a5..917e3f620d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Tools/Edit workspace boundary errors: preserve the real `Path escapes workspace root` failure path instead of surfacing a misleading access/file-not-found error when editing outside workspace roots. Landed from contributor PR #31015 by @haosenwang1018. Thanks @haosenwang1018. - Sandbox/mkdirp boundary checks: allow directory-safe boundary validation for existing in-boundary subdirectories, preventing false `cannot create directories` failures in sandbox write mode. (#30610) Thanks @glitch418x. - Android/Voice screen TTS: stream assistant speech via ElevenLabs WebSocket in Talk Mode, stop cleanly on speaker mute/barge-in, and ignore stale out-of-order stream events. (#29521) Thanks @gregmousseau. - Web UI/Cron: include configured agent model defaults/fallbacks in cron model suggestions so scheduled-job model autocomplete reflects configured models. (#29709) Thanks @Sid-Qin. diff --git a/src/agents/pi-tools.read.host-edit-access.test.ts b/src/agents/pi-tools.read.host-edit-access.test.ts index ca85c496148..a065fb89a59 100644 --- a/src/agents/pi-tools.read.host-edit-access.test.ts +++ b/src/agents/pi-tools.read.host-edit-access.test.ts @@ -43,7 +43,7 @@ describe("createHostWorkspaceEditTool host access mapping", () => { }); it.runIf(process.platform !== "win32")( - "maps outside-workspace safe-open failures to EACCES", + "silently passes access for outside-workspace paths so readFile reports the real error", async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-edit-access-test-")); const workspaceDir = path.join(tmpDir, "workspace"); @@ -58,9 +58,13 @@ describe("createHostWorkspaceEditTool host access mapping", () => { createHostWorkspaceEditTool(workspaceDir, { workspaceOnly: true }); expect(mocks.operations).toBeDefined(); + // access must NOT throw for outside-workspace paths; the upstream + // library replaces any access error with a misleading "File not found". + // By resolving silently the subsequent readFile call surfaces the real + // "Path escapes workspace root" / "outside-workspace" error instead. await expect( mocks.operations!.access(path.join(workspaceDir, "escape", "secret.txt")), - ).rejects.toMatchObject({ code: "EACCES" }); + ).resolves.toBeUndefined(); }, ); }); diff --git a/src/agents/pi-tools.read.ts b/src/agents/pi-tools.read.ts index 226a9f60eb7..f4a31d6041e 100644 --- a/src/agents/pi-tools.read.ts +++ b/src/agents/pi-tools.read.ts @@ -843,7 +843,17 @@ function createHostEditOperations(root: string, options?: { workspaceOnly?: bool }); }, access: async (absolutePath: string) => { - const relative = toRelativePathInRoot(root, absolutePath); + let relative: string; + try { + relative = toRelativePathInRoot(root, absolutePath); + } catch { + // Path escapes workspace root. Don't throw here – the upstream + // library replaces any `access` error with a misleading "File not + // found" message. By returning silently the subsequent `readFile` + // call will throw the same "Path escapes workspace root" error + // through a code-path that propagates the original message. + return; + } try { const opened = await openFileWithinRoot({ rootDir: root, @@ -855,7 +865,9 @@ function createHostEditOperations(root: string, options?: { workspaceOnly?: bool throw createFsAccessError("ENOENT", absolutePath); } if (error instanceof SafeOpenError && error.code === "outside-workspace") { - throw createFsAccessError("EACCES", absolutePath); + // Don't throw here – see the comment above about the upstream + // library swallowing access errors as "File not found". + return; } throw error; }