[codex] Extract filesystem safety primitives (#77918)

* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
This commit is contained in:
Peter Steinberger
2026-05-06 02:15:17 +01:00
committed by GitHub
parent 61481eb34f
commit 538605ff44
356 changed files with 4918 additions and 11913 deletions

View File

@@ -1,3 +1,4 @@
import { realpathSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
@@ -61,7 +62,7 @@ function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
expect(pathValue).not.toContain(key);
expect(pathValue).not.toContain("..");
const tmpRoot = path.resolve(resolvePreferredOpenClawTmpDir());
const tmpRoot = realpathSync(resolvePreferredOpenClawTmpDir());
const resolved = path.resolve(pathValue);
const rel = path.relative(tmpRoot, resolved);
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);

View File

@@ -5,8 +5,10 @@ import type * as Lark from "@larksuiteoapi/node-sdk";
import type { MessageReceipt } from "openclaw/plugin-sdk/channel-message";
import { mediaKindFromMime } from "openclaw/plugin-sdk/media-mime";
import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS, runFfmpeg } from "openclaw/plugin-sdk/media-runtime";
import { readRegularFile } from "openclaw/plugin-sdk/security-runtime";
import {
resolvePreferredOpenClawTmpDir,
withTempWorkspace,
withTempDownloadPath,
} from "openclaw/plugin-sdk/temp-path";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
@@ -421,10 +423,11 @@ export async function uploadImageFeishu(params: {
const { cfg, image, imageType = "message", accountId } = params;
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
// SDK accepts Buffer directly or fs.ReadStream for file paths
// Using Readable.from(buffer) causes issues with form-data library
// SDK accepts Buffer directly. Keep string path support on this helper, but
// verify the path as a regular local file before uploading it.
// See: https://github.com/larksuite/node-sdk/issues/121
const imageData = typeof image === "string" ? fs.createReadStream(image) : image;
const imageData =
typeof image === "string" ? (await readRegularFile({ filePath: image })).buffer : image;
const response = await requestFeishuApi(
() =>
@@ -475,10 +478,11 @@ export async function uploadFileFeishu(params: {
const { cfg, file, fileName, fileType, duration, accountId } = params;
const { client } = createConfiguredFeishuMediaClient({ cfg, accountId });
// SDK accepts Buffer directly or fs.ReadStream for file paths
// Using Readable.from(buffer) causes issues with form-data library
// SDK accepts Buffer directly. Keep string path support on this helper, but
// verify the path as a regular local file before uploading it.
// See: https://github.com/larksuite/node-sdk/issues/121
const fileData = typeof file === "string" ? fs.createReadStream(file) : file;
const fileData =
typeof file === "string" ? (await readRegularFile({ filePath: file })).buffer : file;
const safeFileName = sanitizeFileNameForUpload(fileName);
@@ -747,45 +751,42 @@ async function transcodeToFeishuVoiceOpus(params: {
fileName: string;
contentType?: string;
}): Promise<{ buffer: Buffer; fileName: string; contentType: string }> {
const tempRoot = resolvePreferredOpenClawTmpDir();
await fs.promises.mkdir(tempRoot, { recursive: true, mode: 0o700 });
const tempDir = await fs.promises.mkdtemp(path.join(tempRoot, "feishu-voice-"));
try {
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
const inputExt = ext && ext.length <= 12 ? ext : ".audio";
const inputPath = path.join(tempDir, `input${inputExt}`);
const outputPath = path.join(tempDir, FEISHU_VOICE_FILE_NAME);
await fs.promises.writeFile(inputPath, params.buffer, { mode: 0o600 });
await runFfmpeg([
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
inputPath,
"-vn",
"-sn",
"-dn",
"-t",
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
"-ar",
String(FEISHU_VOICE_SAMPLE_RATE_HZ),
"-ac",
"1",
"-c:a",
"libopus",
"-b:a",
FEISHU_VOICE_BITRATE,
outputPath,
]);
return {
buffer: await fs.promises.readFile(outputPath),
fileName: FEISHU_VOICE_FILE_NAME,
contentType: "audio/ogg",
};
} finally {
await fs.promises.rm(tempDir, { recursive: true, force: true });
}
return await withTempWorkspace(
{ rootDir: resolvePreferredOpenClawTmpDir(), prefix: "feishu-voice-" },
async (workspace) => {
const ext = normalizeLowercaseStringOrEmpty(path.extname(params.fileName));
const inputExt = ext && ext.length <= 12 ? ext : ".audio";
const inputPath = await workspace.write(`input${inputExt}`, params.buffer);
const outputPath = workspace.path(FEISHU_VOICE_FILE_NAME);
await runFfmpeg([
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
inputPath,
"-vn",
"-sn",
"-dn",
"-t",
String(MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS),
"-ar",
String(FEISHU_VOICE_SAMPLE_RATE_HZ),
"-ac",
"1",
"-c:a",
"libopus",
"-b:a",
FEISHU_VOICE_BITRATE,
outputPath,
]);
return {
buffer: await workspace.read(FEISHU_VOICE_FILE_NAME),
fileName: FEISHU_VOICE_FILE_NAME,
contentType: "audio/ogg",
};
},
);
}
async function prepareFeishuVoiceMedia(params: {

View File

@@ -1,4 +1,3 @@
import fs from "node:fs";
import path from "node:path";
import {
attachChannelToResult,
@@ -18,6 +17,7 @@ import {
sendPayloadMediaSequenceAndFinalize,
sendTextMediaPayload,
} from "openclaw/plugin-sdk/reply-payload";
import { statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import { resolveFeishuAccount } from "./accounts.js";
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
@@ -66,18 +66,12 @@ function normalizePossibleLocalImagePath(text: string | undefined): string | nul
if (!path.isAbsolute(raw)) {
return null;
}
if (!fs.existsSync(raw)) {
return null;
}
// Fix race condition: wrap statSync in try-catch to handle file deletion
// between existsSync and statSync
try {
if (!fs.statSync(raw).isFile()) {
const stat = statRegularFileSync(raw);
if (stat.missing) {
return null;
}
} catch {
// File may have been deleted or became inaccessible between checks
return null;
}