[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

@@ -3,7 +3,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { openRootFileSync } from "../infra/boundary-file-read.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import {
@@ -122,7 +122,7 @@ function loadExternalChannelSecretContractFromRecord(
if (!contractPath) {
return undefined;
}
const opened = openBoundaryFileSync({
const opened = openRootFileSync({
absolutePath: contractPath,
rootPath: record.rootDir,
boundaryLabel: "plugin root",

View File

@@ -400,16 +400,14 @@ describe("secret ref resolver", () => {
}),
);
const originalReadFile = fs.readFile.bind(fs);
const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation(((
targetPath: Parameters<typeof fs.readFile>[0],
options?: Parameters<typeof fs.readFile>[1],
) => {
if (typeof targetPath === "string" && targetPath === filePath) {
return new Promise<Buffer>(() => {});
}
return originalReadFile(targetPath, options);
}) as typeof fs.readFile);
const sampleHandle = await fs.open(filePath, "r");
const fileHandlePrototype = Object.getPrototypeOf(sampleHandle) as {
readFile: typeof sampleHandle.readFile;
};
await sampleHandle.close();
const readFileSpy = vi
.spyOn(fileHandlePrototype, "readFile")
.mockImplementation(() => new Promise<Buffer>(() => {}) as never);
try {
await expect(

View File

@@ -10,6 +10,7 @@ import type {
SecretRefSource,
} from "../config/types.secrets.js";
import { formatErrorMessage } from "../infra/errors.js";
import { FsSafeError, readSecureFile } from "../infra/fs-safe.js";
import { inspectPathPermissions, safeStat } from "../security/audit-fs.js";
import { isPathInside } from "../security/scan-paths.js";
import { resolveUserPath } from "../utils.js";
@@ -283,33 +284,18 @@ async function readFileProviderPayload(params: {
const filePath = resolveUserPath(params.providerConfig.path);
const readPromise = (async () => {
const secureFilePath = await assertSecurePath({
targetPath: filePath,
label: `secrets.providers.${params.providerName}.path`,
allowInsecurePath: params.providerConfig.allowInsecurePath,
});
const timeoutMs = normalizePositiveInt(
params.providerConfig.timeoutMs,
DEFAULT_FILE_TIMEOUT_MS,
);
const maxBytes = normalizePositiveInt(params.providerConfig.maxBytes, DEFAULT_FILE_MAX_BYTES);
const abortController = new AbortController();
const timeoutErrorMessage = `File provider "${params.providerName}" timed out after ${timeoutMs}ms.`;
let timeoutHandle: NodeJS.Timeout | null = null;
const timeoutPromise = new Promise<never>((_resolve, reject) => {
timeoutHandle = setTimeout(() => {
abortController.abort();
reject(new Error(timeoutErrorMessage));
}, timeoutMs);
});
try {
const payload = await Promise.race([
fs.readFile(secureFilePath, { signal: abortController.signal }),
timeoutPromise,
]);
if (payload.byteLength > maxBytes) {
throw new Error(`File provider "${params.providerName}" exceeded maxBytes (${maxBytes}).`);
}
const { buffer: payload } = await readSecureFile({
filePath,
label: `secrets.providers.${params.providerName}.path`,
io: { maxBytes, timeoutMs },
permissions: { allowInsecure: params.providerConfig.allowInsecurePath },
});
const text = payload.toString("utf8").replace(/^\uFEFF/, "");
if (params.providerConfig.mode === "singleValue") {
return text.replace(/\r?\n$/, "");
@@ -320,14 +306,12 @@ async function readFileProviderPayload(params: {
}
return parsed;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(timeoutErrorMessage, { cause: error });
if (error instanceof FsSafeError && error.code === "timeout") {
throw new Error(`File provider "${params.providerName}" timed out after ${timeoutMs}ms.`, {
cause: error,
});
}
throw error;
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
})();

View File

@@ -1,5 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { privateFileStoreSync } from "../infra/private-file-store.js";
import { replaceFileAtomicSync } from "../infra/replace-file.js";
export { isRecord } from "../utils.js";
export function isNonEmptyString(value: unknown): value is string {
@@ -40,9 +42,9 @@ export function ensureDirForFile(filePath: string): void {
}
export function writeJsonFileSecure(pathname: string, value: unknown): void {
ensureDirForFile(pathname);
fs.writeFileSync(pathname, `${JSON.stringify(value, null, 2)}\n`, "utf8");
fs.chmodSync(pathname, 0o600);
privateFileStoreSync(path.dirname(pathname)).writeJson(path.basename(pathname), value, {
trailingNewline: true,
});
}
export function readTextFileIfExists(pathname: string): string | null {
@@ -53,9 +55,14 @@ export function readTextFileIfExists(pathname: string): string | null {
}
export function writeTextFileAtomic(pathname: string, value: string, mode = 0o600): void {
ensureDirForFile(pathname);
const tempPath = `${pathname}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tempPath, value, "utf8");
fs.chmodSync(tempPath, mode);
fs.renameSync(tempPath, pathname);
if (mode !== 0o600) {
replaceFileAtomicSync({
filePath: pathname,
content: value,
mode,
tempPrefix: ".openclaw-secrets",
});
return;
}
privateFileStoreSync(path.dirname(pathname)).writeText(path.basename(pathname), value);
}