mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:40:49 +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
@@ -21,7 +21,8 @@ export {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeResolvedSecretInputString,
|
||||
} from "./host/openclaw-runtime-config.js";
|
||||
export { writeFileWithinRoot } from "./host/openclaw-runtime-io.js";
|
||||
export { root } from "./host/openclaw-runtime-io.js";
|
||||
export { isPathInside } from "./host/fs-utils.js";
|
||||
export { createSubsystemLogger } from "./host/openclaw-runtime-io.js";
|
||||
export { detectMime } from "./host/openclaw-runtime-io.js";
|
||||
export { resolveGlobalSingleton } from "./host/openclaw-runtime-io.js";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type SessionSendPolicyConfig,
|
||||
splitShellArgs,
|
||||
} from "./config-utils.js";
|
||||
import { isPathInside } from "./fs-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
export type ResolvedMemoryBackendConfig = {
|
||||
@@ -143,11 +144,10 @@ function canonicalizePathForContainment(rawPath: string): string {
|
||||
}
|
||||
|
||||
function isPathInsideRoot(candidatePath: string, rootPath: string): boolean {
|
||||
const relative = path.relative(
|
||||
return isPathInside(
|
||||
canonicalizePathForContainment(rootPath),
|
||||
canonicalizePathForContainment(candidatePath),
|
||||
);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function ensureUniqueName(base: string, existing: Set<string>): string {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { Stats } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
export type RegularFileStatResult = { missing: true } | { missing: false; stat: Stats };
|
||||
import "../../../../src/infra/fs-safe-defaults.js";
|
||||
export { isPathInside } from "@openclaw/fs-safe/path";
|
||||
export {
|
||||
readRegularFile,
|
||||
statRegularFile,
|
||||
type RegularFileStatResult,
|
||||
} from "@openclaw/fs-safe/advanced";
|
||||
export { walkDirectory, type WalkDirectoryEntry } from "@openclaw/fs-safe/walk";
|
||||
|
||||
export function isFileMissingError(
|
||||
err: unknown,
|
||||
@@ -13,19 +17,3 @@ export function isFileMissingError(
|
||||
(err as Partial<NodeJS.ErrnoException>).code === "ENOENT",
|
||||
);
|
||||
}
|
||||
|
||||
export async function statRegularFile(absPath: string): Promise<RegularFileStatResult> {
|
||||
let stat: Stats;
|
||||
try {
|
||||
stat = await fs.lstat(absPath);
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return { missing: true };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (stat.isSymbolicLink() || !stat.isFile()) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
return { missing: false, stat };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ import path from "node:path";
|
||||
import { CANONICAL_ROOT_MEMORY_FILENAME } from "./config-utils.js";
|
||||
import { estimateStructuredEmbeddingInputBytes } from "./embedding-input-limits.js";
|
||||
import { buildTextEmbeddingInput, type EmbeddingInput } from "./embedding-inputs.js";
|
||||
import { isFileMissingError } from "./fs-utils.js";
|
||||
import {
|
||||
isFileMissingError,
|
||||
readRegularFile,
|
||||
statRegularFile,
|
||||
walkDirectory,
|
||||
type WalkDirectoryEntry,
|
||||
} from "./fs-utils.js";
|
||||
import {
|
||||
buildMemoryMultimodalLabel,
|
||||
classifyMemoryMultimodalPath,
|
||||
@@ -103,36 +109,31 @@ function isAllowedMemoryFilePath(filePath: string, multimodal?: MemoryMultimodal
|
||||
);
|
||||
}
|
||||
|
||||
async function walkDir(
|
||||
function shouldDescendMemoryEntry(
|
||||
entry: WalkDirectoryEntry,
|
||||
shouldSkipPath?: (absPath: string) => boolean,
|
||||
): boolean {
|
||||
if (shouldSkipPath?.(entry.path)) {
|
||||
return false;
|
||||
}
|
||||
return entry.kind === "directory" && entry.name !== ".openclaw-repair";
|
||||
}
|
||||
|
||||
async function collectMemoryFilesFromDir(
|
||||
dir: string,
|
||||
files: string[],
|
||||
multimodal?: MemoryMultimodalSettings,
|
||||
shouldSkipPath?: (absPath: string) => boolean,
|
||||
) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (shouldSkipPath?.(full)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === ".openclaw-repair") {
|
||||
continue;
|
||||
}
|
||||
await walkDir(full, files, multimodal, shouldSkipPath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (!isAllowedMemoryFilePath(full, multimodal)) {
|
||||
continue;
|
||||
}
|
||||
files.push(full);
|
||||
}
|
||||
): Promise<void> {
|
||||
const scan = await walkDirectory(dir, {
|
||||
symlinks: "skip",
|
||||
descend: (entry) => shouldDescendMemoryEntry(entry, shouldSkipPath),
|
||||
include: (entry) =>
|
||||
!shouldSkipPath?.(entry.path) &&
|
||||
entry.kind === "file" &&
|
||||
isAllowedMemoryFilePath(entry.path, multimodal),
|
||||
});
|
||||
files.push(...scan.entries.map((entry) => entry.path));
|
||||
}
|
||||
|
||||
export async function listMemoryFiles(
|
||||
@@ -148,8 +149,8 @@ export async function listMemoryFiles(
|
||||
|
||||
const addMarkdownFile = async (absPath: string) => {
|
||||
try {
|
||||
const stat = await fs.lstat(absPath);
|
||||
if (stat.isSymbolicLink() || !stat.isFile()) {
|
||||
const stat = await statRegularFile(absPath);
|
||||
if (stat.missing) {
|
||||
return;
|
||||
}
|
||||
if (!absPath.endsWith(".md")) {
|
||||
@@ -166,7 +167,7 @@ export async function listMemoryFiles(
|
||||
try {
|
||||
const dirStat = await fs.lstat(memoryDir);
|
||||
if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
|
||||
await walkDir(memoryDir, result, multimodal, shouldSkipWorkspaceMemoryPath);
|
||||
await collectMemoryFilesFromDir(memoryDir, result, multimodal, shouldSkipWorkspaceMemoryPath);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -182,7 +183,12 @@ export async function listMemoryFiles(
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
await walkDir(inputPath, result, multimodal, shouldSkipWorkspaceMemoryPath);
|
||||
await collectMemoryFilesFromDir(
|
||||
inputPath,
|
||||
result,
|
||||
multimodal,
|
||||
shouldSkipWorkspaceMemoryPath,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (stat.isFile() && isAllowedMemoryFilePath(inputPath, multimodal)) {
|
||||
@@ -215,15 +221,11 @@ export async function buildFileEntry(
|
||||
workspaceDir: string,
|
||||
multimodal?: MemoryMultimodalSettings,
|
||||
): Promise<MemoryFileEntry | null> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(absPath);
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
const regularFile = await statRegularFile(absPath);
|
||||
if (regularFile.missing) {
|
||||
return null;
|
||||
}
|
||||
const stat = regularFile.stat;
|
||||
const normalizedPath = path.relative(workspaceDir, absPath).replace(/\\/g, "/");
|
||||
const multimodalSettings = multimodal ?? DISABLED_MULTIMODAL_SETTINGS;
|
||||
const modality = classifyMemoryMultimodalPath(absPath, multimodalSettings);
|
||||
@@ -233,7 +235,12 @@ export async function buildFileEntry(
|
||||
}
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await fs.readFile(absPath);
|
||||
buffer = (
|
||||
await readRegularFile({
|
||||
filePath: absPath,
|
||||
maxBytes: multimodalSettings.maxFileBytes,
|
||||
})
|
||||
).buffer;
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return null;
|
||||
@@ -269,7 +276,7 @@ export async function buildFileEntry(
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(absPath, "utf-8");
|
||||
content = (await readRegularFile({ filePath: absPath })).buffer.toString("utf-8");
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return null;
|
||||
@@ -296,21 +303,17 @@ async function loadMultimodalEmbeddingInput(
|
||||
if (entry.kind !== "multimodal" || !entry.contentText || !entry.mimeType) {
|
||||
return null;
|
||||
}
|
||||
let stat;
|
||||
try {
|
||||
stat = await fs.stat(entry.absPath);
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
const regularFile = await statRegularFile(entry.absPath);
|
||||
if (regularFile.missing) {
|
||||
return null;
|
||||
}
|
||||
const stat = regularFile.stat;
|
||||
if (stat.size !== entry.size) {
|
||||
return null;
|
||||
}
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await fs.readFile(entry.absPath);
|
||||
buffer = (await readRegularFile({ filePath: entry.absPath, maxBytes: entry.size })).buffer;
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
DEFAULT_SQLITE_WAL_TRUNCATE_INTERVAL_MS,
|
||||
applyWindowsSpawnProgramPolicy,
|
||||
configureSqliteWalMaintenance,
|
||||
root,
|
||||
createSubsystemLogger,
|
||||
detectMime,
|
||||
estimateStringChars,
|
||||
@@ -21,7 +22,6 @@ export {
|
||||
shouldIgnoreWarning,
|
||||
splitShellArgs,
|
||||
truncateUtf16Safe,
|
||||
writeFileWithinRoot,
|
||||
} from "./openclaw-runtime.js";
|
||||
|
||||
export type {
|
||||
|
||||
@@ -74,7 +74,7 @@ export { isVerbose, setVerbose } from "../../../../src/globals.js";
|
||||
|
||||
// IO, network, and logging helpers.
|
||||
export { isExecCompletionEvent } from "../../../../src/infra/heartbeat-events-filter.js";
|
||||
export { writeFileWithinRoot } from "../../../../src/infra/fs-safe.js";
|
||||
export { root } from "../../../../src/infra/fs-safe.js";
|
||||
export { fetchWithSsrFGuard } from "../../../../src/infra/net/fetch-guard.js";
|
||||
export { shouldUseEnvHttpProxyForUrl } from "../../../../src/infra/net/proxy-env.js";
|
||||
export { ssrfPolicyFromHttpBaseUrlAllowedHostname } from "../../../../src/infra/net/ssrf.js";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
resolveMemorySearchConfig,
|
||||
type OpenClawConfig,
|
||||
} from "./config-utils.js";
|
||||
import { isFileMissingError, statRegularFile } from "./fs-utils.js";
|
||||
import { isFileMissingError, isPathInside, readRegularFile, statRegularFile } from "./fs-utils.js";
|
||||
import { isMemoryPath, normalizeExtraMemoryPaths } from "./internal.js";
|
||||
import {
|
||||
buildMemoryReadResult,
|
||||
@@ -43,7 +43,11 @@ export async function readMemoryFile(params: {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
if (absPath === additionalPath || absPath.startsWith(`${additionalPath}${path.sep}`)) {
|
||||
if (isPathInside(additionalPath, absPath)) {
|
||||
const candidateStat = await fs.lstat(absPath).catch(() => null);
|
||||
if (candidateStat?.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
allowedAdditional = true;
|
||||
break;
|
||||
}
|
||||
@@ -68,7 +72,7 @@ export async function readMemoryFile(params: {
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(absPath, "utf-8");
|
||||
content = (await readRegularFile({ filePath: absPath })).buffer.toString("utf-8");
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return { text: "", path: relPath };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readRegularFile, statRegularFile } from "./fs-utils.js";
|
||||
import { hashText } from "./hash.js";
|
||||
import { createSubsystemLogger, redactSensitiveText } from "./openclaw-runtime-io.js";
|
||||
import {
|
||||
@@ -524,7 +525,11 @@ export async function buildSessionEntry(
|
||||
opts: BuildSessionEntryOptions = {},
|
||||
): Promise<SessionFileEntry | null> {
|
||||
try {
|
||||
const stat = await fs.stat(absPath);
|
||||
const regularFile = await statRegularFile(absPath);
|
||||
if (regularFile.missing) {
|
||||
return null;
|
||||
}
|
||||
const stat = regularFile.stat;
|
||||
if (shouldSkipTranscriptFileForDreaming(absPath)) {
|
||||
return {
|
||||
path: sessionPathForFile(absPath),
|
||||
@@ -537,7 +542,7 @@ export async function buildSessionEntry(
|
||||
messageTimestampsMs: [],
|
||||
};
|
||||
}
|
||||
const raw = await fs.readFile(absPath, "utf-8");
|
||||
const raw = (await readRegularFile({ filePath: absPath })).buffer.toString("utf-8");
|
||||
const lines = raw.split("\n");
|
||||
const collected: string[] = [];
|
||||
const lineMap: number[] = [];
|
||||
|
||||
Reference in New Issue
Block a user