From c659590d22eb040b2b3e7125fa0d5c4f06e75a9d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 06:00:05 +0100 Subject: [PATCH] fix: restore external file write helper --- src/infra/fs-safe.test.ts | 16 ++++++++++++++++ src/infra/fs-safe.ts | 33 ++++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/infra/fs-safe.test.ts b/src/infra/fs-safe.test.ts index 6d9b10d160e..399dfb3ef2a 100644 --- a/src/infra/fs-safe.test.ts +++ b/src/infra/fs-safe.test.ts @@ -13,6 +13,7 @@ import { FsSafeError, readLocalFileSafely, root as openRoot, + writeExternalFileWithinRoot, } from "./fs-safe.js"; const tempDirs = createTrackedTempDirs(); @@ -128,6 +129,21 @@ describe("fs-safe", () => { expect((err as FsSafeError).message).not.toMatch(/EISDIR/i); }); + it("writes external command output within an allowed root", async () => { + const dir = await tempDirs.make("openclaw-fs-safe-output-"); + + const result = await writeExternalFileWithinRoot({ + rootDir: dir, + path: "artifact.txt", + write: async (tempPath) => { + await fs.writeFile(tempPath, "artifact"); + }, + }); + + expect(result.path).toBe(path.join(dir, "artifact.txt")); + await expect(fs.readFile(path.join(dir, "artifact.txt"), "utf8")).resolves.toBe("artifact"); + }); + it("enforces maxBytes", async () => { const dir = await tempDirs.make("openclaw-fs-safe-"); const file = path.join(dir, "big.bin"); diff --git a/src/infra/fs-safe.ts b/src/infra/fs-safe.ts index ce7eac30f18..a54900fdc74 100644 --- a/src/infra/fs-safe.ts +++ b/src/infra/fs-safe.ts @@ -1,4 +1,6 @@ import "./fs-safe-defaults.js"; +import path from "node:path"; +import { writeViaSiblingTempPath } from "@openclaw/fs-safe/advanced"; import { root as fsSafeRoot, type ReadResult } from "@openclaw/fs-safe/root"; export { FsSafeError, type FsSafeErrorCode } from "@openclaw/fs-safe/errors"; @@ -45,11 +47,32 @@ export { type WalkDirectoryResult, } from "@openclaw/fs-safe/walk"; export { withTimeout } from "@openclaw/fs-safe/advanced"; -export { - writeExternalFileWithinRoot, - type ExternalFileWriteOptions, - type ExternalFileWriteResult, -} from "@openclaw/fs-safe/output"; + +export type ExternalFileWriteOptions = { + rootDir: string; + path: string; + write: (tempPath: string) => Promise; + fallbackFileName?: string; + tempPrefix?: string; +}; + +export type ExternalFileWriteResult = { + path: string; +}; + +export async function writeExternalFileWithinRoot( + options: ExternalFileWriteOptions, +): Promise { + const targetPath = path.resolve(options.rootDir, options.path); + await writeViaSiblingTempPath({ + rootDir: options.rootDir, + targetPath, + writeTemp: options.write, + fallbackFileName: options.fallbackFileName, + tempPrefix: options.tempPrefix, + }); + return { path: targetPath }; +} /** @deprecated Use root(rootDir).read(relativePath, options). */ export async function readFileWithinRoot(params: {