mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
[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:
committed by
GitHub
parent
61481eb34f
commit
538605ff44
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user