[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,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { assertNoSymlinkParents, pathScope } from "openclaw/plugin-sdk/security-runtime";
export function resolveRepoRelativeOutputDir(repoRoot: string, outputDir?: string) {
if (!outputDir) {
@@ -8,12 +9,11 @@ export function resolveRepoRelativeOutputDir(repoRoot: string, outputDir?: strin
if (path.isAbsolute(outputDir)) {
throw new Error("--output-dir must be a relative path inside the repo root.");
}
const resolved = path.resolve(repoRoot, outputDir);
const relative = path.relative(repoRoot, resolved);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
const resolved = pathScope(repoRoot, { label: "repo root" }).resolve(outputDir);
if (!resolved.ok) {
throw new Error("--output-dir must stay within the repo root.");
}
return resolved;
return resolved.path;
}
async function resolveNearestExistingPath(targetPath: string) {
@@ -44,22 +44,18 @@ function assertRepoRelativePath(repoRoot: string, targetPath: string, label: str
}
async function assertNoSymlinkSegments(repoRoot: string, targetPath: string, label: string) {
const relative = assertRepoRelativePath(repoRoot, targetPath, label);
let current = repoRoot;
for (const segment of relative.split(path.sep).filter((entry) => entry.length > 0)) {
current = path.join(current, segment);
let stats: Awaited<ReturnType<typeof fs.lstat>> | null = null;
try {
stats = await fs.lstat(current);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
break;
}
throw error;
}
if (stats.isSymbolicLink()) {
throw new Error(`${label} must not traverse symlinks.`);
assertRepoRelativePath(repoRoot, targetPath, label);
try {
await assertNoSymlinkParents({
rootDir: repoRoot,
targetPath,
messagePrefix: label,
});
} catch (error) {
if (error instanceof Error && error.message.includes("symlink")) {
throw new Error(`${label} must not traverse symlinks.`, { cause: error });
}
throw error;
}
}
@@ -81,40 +77,10 @@ export async function ensureRepoBoundDirectory(
label: string,
opts?: { mode?: number },
) {
const repoRootResolved = path.resolve(repoRoot);
const targetResolved = path.resolve(targetDir);
const relative = assertRepoRelativePath(repoRootResolved, targetResolved, label);
const repoRootReal = await fs.realpath(repoRootResolved);
let current = repoRootResolved;
for (const segment of relative.split(path.sep).filter((entry) => entry.length > 0)) {
current = path.join(current, segment);
while (true) {
try {
const stats = await fs.lstat(current);
if (stats.isSymbolicLink()) {
throw new Error(`${label} must not traverse symlinks.`);
}
if (!stats.isDirectory()) {
throw new Error(`${label} must point to a directory.`);
}
break;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
throw error;
}
try {
await fs.mkdir(current, { recursive: false, mode: opts?.mode });
} catch (mkdirError) {
if ((mkdirError as NodeJS.ErrnoException).code === "EEXIST") {
continue;
}
throw mkdirError;
}
}
}
await assertNoSymlinkSegments(path.resolve(repoRoot), path.resolve(targetDir), label);
const result = await pathScope(repoRoot, { label }).ensureDir(targetDir, { mode: opts?.mode });
if (!result.ok) {
throw new Error(`${label} must stay within the repo root.`);
}
const targetReal = await fs.realpath(targetResolved);
assertRepoRelativePath(repoRootReal, targetReal, label);
return targetResolved;
return result.path;
}

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js";
export type MantisDesktopBrowserSmokeOptions = {
@@ -146,15 +147,6 @@ async function defaultCommandRunner(
});
}
async function pathExists(filePath: string) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function resolveCrabboxBin(params: {
env: NodeJS.ProcessEnv;
explicit?: string;

View File

@@ -2,6 +2,7 @@ import { spawn, type SpawnOptions } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js";
import {
acquireQaCredentialLease,
@@ -255,15 +256,6 @@ async function defaultCommandRunner(
});
}
async function pathExists(filePath: string) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function readRemoteMetadata(
outputDir: string,
): Promise<SlackDesktopRemoteMetadata | undefined> {
@@ -289,7 +281,6 @@ async function readRemoteMetadata(
return undefined;
}
}
async function resolveCrabboxBin(params: {
env: NodeJS.ProcessEnv;
explicit?: string;

View File

@@ -2,6 +2,7 @@ import { spawn, type SpawnOptions } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { ensureRepoBoundDirectory, resolveRepoRelativeOutputDir } from "../cli-paths.js";
export type MantisVisualTaskVisionMode = "image-describe" | "metadata";
@@ -211,15 +212,6 @@ async function defaultCommandRunner(
});
}
async function pathExists(filePath: string) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
async function nonEmptyFileExists(filePath: string) {
try {
const stat = await fs.stat(filePath);

View File

@@ -1,8 +1,9 @@
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import { access, appendFile, mkdir, writeFile } from "node:fs/promises";
import { access, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import type { QaProviderMode } from "./model-selection.js";
import { resolveQaForwardedLiveEnv, resolveQaLiveProviderConfigPath } from "./providers/env.js";
@@ -432,7 +433,7 @@ export function renderQaMultipassGuestScript(
}
async function appendMultipassLog(logPath: string, message: string) {
await appendFile(logPath, message, "utf8");
await appendRegularFile({ filePath: logPath, content: message });
}
async function runMultipassCommand(logPath: string, args: string[], options: ExecFileOptions = {}) {

View File

@@ -1,20 +1,23 @@
import fs from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import {
tempWorkspace,
resolvePreferredOpenClawTmpDir,
type TempWorkspace,
} from "openclaw/plugin-sdk/temp-path";
export function createTempDirHarness() {
const tempDirs: string[] = [];
const tempDirs: TempWorkspace[] = [];
return {
async cleanup() {
await Promise.all(
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
await Promise.all(tempDirs.splice(0).map((dir) => dir.cleanup()));
},
async makeTempDir(prefix: string) {
const dir = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), prefix));
const dir = await tempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix,
});
tempDirs.push(dir);
return dir;
return dir.dir;
},
};
}