[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,9 +1,9 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import { compileMemoryWikiVault, type CompileMemoryWikiResult } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import {
@@ -150,22 +150,23 @@ function buildSynthesisBody(params: {
}
async function writeWikiPage(params: {
absolutePath: string;
rootDir: string;
relativePath: string;
frontmatter: Record<string, unknown>;
body: string;
}): Promise<boolean> {
const root = await fsRoot(params.rootDir);
const rendered = withTrailingNewline(
renderWikiMarkdown({
frontmatter: params.frontmatter,
body: params.body,
}),
);
const existing = await fs.readFile(params.absolutePath, "utf8").catch(() => "");
const existing = await root.readText(params.relativePath).catch(() => "");
if (existing === rendered) {
return false;
}
await fs.mkdir(path.dirname(params.absolutePath), { recursive: true });
await fs.writeFile(params.absolutePath, rendered, "utf8");
await root.write(params.relativePath, rendered);
return true;
}
@@ -183,14 +184,15 @@ async function applyCreateSynthesisMutation(params: {
}): Promise<{ changed: boolean; pagePath: string; pageId: string }> {
const slug = slugifyWikiSegment(params.mutation.title);
const pagePath = path.join("syntheses", `${slug}.md`).replace(/\\/g, "/");
const absolutePath = path.join(params.config.vault.path, pagePath);
const existing = await fs.readFile(absolutePath, "utf8").catch(() => "");
const root = await fsRoot(params.config.vault.path);
const existing = await root.readText(pagePath).catch(() => "");
const parsed = parseWikiMarkdown(existing);
const pageId =
(typeof parsed.frontmatter.id === "string" && parsed.frontmatter.id.trim()) ||
`synthesis.${slug}`;
const changed = await writeWikiPage({
absolutePath,
rootDir: params.config.vault.path,
relativePath: pagePath,
frontmatter: {
...parsed.frontmatter,
pageType: "synthesis",
@@ -278,7 +280,8 @@ async function applyUpdateMetadataMutation(params: {
}
const parsed = parseWikiMarkdown(page.raw);
const changed = await writeWikiPage({
absolutePath: page.absolutePath,
rootDir: params.config.vault.path,
relativePath: page.relativePath,
frontmatter: buildUpdatedFrontmatter({
original: parsed.frontmatter,
mutation: params.mutation,

View File

@@ -4,6 +4,7 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import {
assessClaimFreshness,
@@ -768,12 +769,13 @@ async function refreshPageRelatedBlocks(params: {
if (!params.config.render.createBacklinks) {
return [];
}
const root = await fsRoot(params.config.vault.path);
const updatedFiles: string[] = [];
for (const page of params.pages) {
if (page.kind === "report") {
continue;
}
const original = await fs.readFile(page.absolutePath, "utf8");
const original = await root.readText(page.relativePath);
const updated = withTrailingNewline(
replaceManagedMarkdownBlock({
original,
@@ -790,7 +792,7 @@ async function refreshPageRelatedBlocks(params: {
if (updated === original) {
continue;
}
await fs.writeFile(page.absolutePath, updated, "utf8");
await root.write(page.relativePath, updated);
updatedFiles.push(page.absolutePath);
}
return updatedFiles;
@@ -817,13 +819,15 @@ function renderSectionList(params: {
}
async function writeManagedMarkdownFile(params: {
filePath: string;
rootDir: string;
relativePath: string;
title: string;
startMarker: string;
endMarker: string;
body: string;
}): Promise<boolean> {
const original = await fs.readFile(params.filePath, "utf8").catch(() => `# ${params.title}\n`);
const root = await fsRoot(params.rootDir);
const original = await root.readText(params.relativePath).catch(() => `# ${params.title}\n`);
const updated = replaceManagedMarkdownBlock({
original,
heading: "## Generated",
@@ -835,7 +839,7 @@ async function writeManagedMarkdownFile(params: {
if (rendered === original) {
return false;
}
await fs.writeFile(params.filePath, rendered, "utf8");
await root.write(params.relativePath, rendered);
return true;
}
@@ -846,8 +850,8 @@ async function writeDashboardPage(params: {
pages: WikiPageSummary[];
now: Date;
}): Promise<boolean> {
const filePath = path.join(params.rootDir, params.definition.relativePath);
const original = await fs.readFile(filePath, "utf8").catch(() =>
const root = await fsRoot(params.rootDir);
const original = await root.readText(params.definition.relativePath).catch(() =>
renderWikiMarkdown({
frontmatter: {
pageType: "report",
@@ -911,7 +915,7 @@ async function writeDashboardPage(params: {
body: updatedBody,
}),
);
await fs.writeFile(filePath, rendered, "utf8");
await root.write(params.definition.relativePath, rendered);
return true;
}
@@ -1267,11 +1271,13 @@ async function writeAgentDigestArtifacts(params: {
[agentDigestPath, agentDigest],
[claimsDigestPath, claimsDigest],
] as const) {
const existing = await fs.readFile(filePath, "utf8").catch(() => "");
const relativePath = path.relative(params.rootDir, filePath);
const root = await fsRoot(params.rootDir);
const existing = await root.readText(relativePath).catch(() => "");
if (existing === content) {
continue;
}
await fs.writeFile(filePath, content, "utf8");
await root.write(relativePath, content);
updatedFiles.push(filePath);
}
return updatedFiles;
@@ -1303,7 +1309,8 @@ export async function compileMemoryWikiVault(
const rootIndexPath = path.join(rootDir, "index.md");
if (
await writeManagedMarkdownFile({
filePath: rootIndexPath,
rootDir,
relativePath: "index.md",
title: "Wiki Index",
startMarker: "<!-- openclaw:wiki:index:start -->",
endMarker: "<!-- openclaw:wiki:index:end -->",
@@ -1314,10 +1321,12 @@ export async function compileMemoryWikiVault(
}
for (const group of COMPILE_PAGE_GROUPS) {
const filePath = path.join(rootDir, group.dir, "index.md");
const relativePath = path.join(group.dir, "index.md").replace(/\\/g, "/");
const filePath = path.join(rootDir, relativePath);
if (
await writeManagedMarkdownFile({
filePath,
rootDir,
relativePath,
title: group.heading,
startMarker: `<!-- openclaw:wiki:${group.dir}:index:start -->`,
endMarker: `<!-- openclaw:wiki:${group.dir}:index:end -->`,

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { compileMemoryWikiVault } from "./compile.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
@@ -16,13 +17,6 @@ type IngestMemoryWikiSourceResult = {
indexUpdatedFiles: string[];
};
function pathExists(filePath: string): Promise<boolean> {
return fs
.access(filePath)
.then(() => true)
.catch(() => false);
}
function resolveSourceTitle(sourcePath: string, explicitTitle?: string): string {
if (explicitTitle?.trim()) {
return explicitTitle.trim();

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
type MemoryWikiLogEntry = {
type: "init" | "ingest" | "compile" | "lint";
@@ -13,5 +14,9 @@ export async function appendMemoryWikiLog(
): Promise<void> {
const logPath = path.join(vaultRoot, ".openclaw-wiki", "log.jsonl");
await fs.mkdir(path.dirname(logPath), { recursive: true });
await fs.appendFile(logPath, `${JSON.stringify(entry)}\n`, "utf8");
await appendRegularFile({
filePath: logPath,
content: `${JSON.stringify(entry)}\n`,
rejectSymlinkParents: true,
});
}

View File

@@ -1,7 +1,5 @@
import { randomUUID } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { FsSafeError, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import {
setImportedSourceEntry,
shouldSkipImportedSourceWrite,
@@ -9,123 +7,6 @@ import {
} from "./source-sync-state.js";
type ImportedSourceState = Parameters<typeof shouldSkipImportedSourceWrite>[0]["state"];
type FileStats = Awaited<ReturnType<typeof fs.lstat>>;
function isPathInside(parent: string, child: string): boolean {
const relative = path.relative(parent, child);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
async function resolveWritableVaultPagePath(params: {
vaultRoot: string;
pagePath: string;
}): Promise<{
pageAbsPath: string;
pageDir: string;
pageDirRealPath: string;
vaultRealPath: string;
existing: FileStats | null;
}> {
const vaultAbsPath = path.resolve(params.vaultRoot);
const pageAbsPath = path.resolve(vaultAbsPath, params.pagePath);
if (!isPathInside(vaultAbsPath, pageAbsPath)) {
throw new Error(`Refusing to write imported source page outside vault: ${params.pagePath}`);
}
const vaultRealPath = await fs.realpath(vaultAbsPath);
const pageDir = path.dirname(pageAbsPath);
await fs.mkdir(pageDir, { recursive: true });
const pageDirRealPath = await fs.realpath(pageDir);
if (!isPathInside(vaultRealPath, pageDirRealPath)) {
throw new Error(`Refusing to write imported source page outside vault: ${params.pagePath}`);
}
const existing = await fs.lstat(pageAbsPath).catch((err: unknown) => {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
return null;
}
throw err;
});
if (existing?.isSymbolicLink()) {
throw new Error(`Refusing to write imported source page through symlink: ${params.pagePath}`);
}
if (existing && !existing.isFile()) {
throw new Error(`Refusing to write imported source page over non-file: ${params.pagePath}`);
}
return { pageAbsPath, pageDir, pageDirRealPath, vaultRealPath, existing };
}
async function assertWritablePageDir(params: {
pageDir: string;
pageDirRealPath: string;
vaultRealPath: string;
pagePath: string;
}): Promise<void> {
const currentPageDirRealPath = await fs.realpath(params.pageDir);
if (
currentPageDirRealPath !== params.pageDirRealPath ||
!isPathInside(params.vaultRealPath, currentPageDirRealPath)
) {
throw new Error(`Refusing to write imported source page outside vault: ${params.pagePath}`);
}
}
async function validateDestinationForReplace(filePath: string, pagePath: string): Promise<void> {
const existing = await fs.lstat(filePath).catch((err: unknown) => {
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
return null;
}
throw err;
});
if (existing?.isSymbolicLink()) {
throw new Error(`Refusing to write imported source page through symlink: ${pagePath}`);
}
if (existing && !existing.isFile()) {
throw new Error(`Refusing to write imported source page over non-file: ${pagePath}`);
}
}
async function writeFileAtomicInVault(params: {
filePath: string;
pageDir: string;
pageDirRealPath: string;
vaultRealPath: string;
pagePath: string;
content: string;
}): Promise<void> {
const noFollow = fsConstants.O_NOFOLLOW ?? 0;
await assertWritablePageDir(params);
const tempPath = path.join(params.pageDir, `.openclaw-wiki-${process.pid}-${randomUUID()}.tmp`);
let shouldRemoveTemp = true;
try {
const handle = await fs.open(
tempPath,
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow,
0o600,
);
try {
const tempStat = await handle.stat();
if (!tempStat.isFile() || tempStat.nlink !== 1) {
throw new Error(
`Refusing to write imported source page through unsafe temp file: ${params.pagePath}`,
);
}
await handle.writeFile(params.content, "utf8");
} finally {
await handle.close();
}
await assertWritablePageDir(params);
await validateDestinationForReplace(params.filePath, params.pagePath);
await fs.rename(tempPath, params.filePath);
shouldRemoveTemp = false;
await assertWritablePageDir(params);
} finally {
if (shouldRemoveTemp) {
await fs.rm(tempPath, { force: true });
}
}
}
export async function writeImportedSourcePage(params: {
vaultRoot: string;
@@ -139,15 +20,15 @@ export async function writeImportedSourcePage(params: {
state: ImportedSourceState;
buildRendered: (raw: string, updatedAt: string) => string;
}): Promise<{ pagePath: string; changed: boolean; created: boolean }> {
const {
pageAbsPath,
pageDir,
pageDirRealPath,
vaultRealPath,
existing: pageStat,
} = await resolveWritableVaultPagePath({
vaultRoot: params.vaultRoot,
pagePath: params.pagePath,
const vault = await fsRoot(params.vaultRoot);
const pageStat = await vault.stat(params.pagePath).catch((error: unknown) => {
if (
error instanceof FsSafeError &&
(error.code === "not-found" || error.code === "path-alias")
) {
return null;
}
throw error;
});
const created = !pageStat;
const updatedAt = new Date(params.sourceUpdatedAtMs).toISOString();
@@ -167,16 +48,22 @@ export async function writeImportedSourcePage(params: {
const raw = await fs.readFile(params.sourcePath, "utf8");
const rendered = params.buildRendered(raw, updatedAt);
const existing = pageStat ? await fs.readFile(pageAbsPath, "utf8").catch(() => "") : "";
const existing = pageStat ? await vault.readText(params.pagePath).catch(() => "") : "";
if (existing !== rendered) {
await writeFileAtomicInVault({
filePath: pageAbsPath,
pageDir,
pageDirRealPath,
vaultRealPath,
pagePath: params.pagePath,
content: rendered,
});
try {
if (pageStat && pageStat.nlink > 1) {
await vault.remove(params.pagePath);
}
await vault.write(params.pagePath, rendered);
} catch (error) {
if (error instanceof FsSafeError) {
throw new Error(
`Refusing to write imported source page through symlink: ${params.pagePath}`,
{ cause: error },
);
}
throw error;
}
}
setImportedSourceEntry({

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { listActiveMemoryPublicArtifacts } from "openclaw/plugin-sdk/memory-host-core";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import type { OpenClawConfig } from "../api.js";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { inferWikiPageKind, toWikiPageSummary, type WikiPageKind } from "./markdown.js";
@@ -65,15 +66,6 @@ type ResolveMemoryWikiStatusDeps = {
resolveCommand?: (command: string) => Promise<string | null>;
};
async function pathExists(inputPath: string): Promise<boolean> {
try {
await fs.access(inputPath);
return true;
} catch {
return false;
}
}
async function collectVaultCounts(vaultPath: string): Promise<{
pageCounts: Record<WikiPageKind, number>;
sourceCounts: MemoryWikiStatus["sourceCounts"];

View File

@@ -4,6 +4,7 @@ import {
replaceManagedMarkdownBlock,
withTrailingNewline,
} from "openclaw/plugin-sdk/memory-host-markdown";
import { FsSafeError, pathExists, root as fsRoot } from "openclaw/plugin-sdk/security-runtime";
import type { ResolvedMemoryWikiConfig } from "./config.js";
import { appendMemoryWikiLog } from "./log.js";
@@ -72,25 +73,22 @@ This vault is maintained by the OpenClaw memory-wiki plugin.
`);
}
async function pathExists(inputPath: string): Promise<boolean> {
try {
await fs.access(inputPath);
return true;
} catch {
return false;
}
}
async function writeFileIfMissing(
filePath: string,
rootDir: string,
relativePath: string,
content: string,
createdFiles: string[],
): Promise<void> {
if (await pathExists(filePath)) {
return;
const root = await fsRoot(rootDir);
try {
await root.create(relativePath, content);
} catch (err) {
if (err instanceof FsSafeError && err.code === "already-exists") {
return;
}
throw err;
}
await fs.writeFile(filePath, content, "utf8");
createdFiles.push(filePath);
createdFiles.push(path.join(rootDir, relativePath));
}
export async function initializeMemoryWikiVault(
@@ -114,20 +112,18 @@ export async function initializeMemoryWikiVault(
await fs.mkdir(fullPath, { recursive: true });
}
await writeFileIfMissing(path.join(rootDir, "AGENTS.md"), buildAgentsMarkdown(), createdFiles);
await writeFileIfMissing(rootDir, "AGENTS.md", buildAgentsMarkdown(), createdFiles);
await writeFileIfMissing(rootDir, "WIKI.md", buildWikiOverviewMarkdown(config), createdFiles);
await writeFileIfMissing(rootDir, "index.md", buildIndexMarkdown(), createdFiles);
await writeFileIfMissing(
path.join(rootDir, "WIKI.md"),
buildWikiOverviewMarkdown(config),
createdFiles,
);
await writeFileIfMissing(path.join(rootDir, "index.md"), buildIndexMarkdown(), createdFiles);
await writeFileIfMissing(
path.join(rootDir, "inbox.md"),
rootDir,
"inbox.md",
withTrailingNewline("# Inbox\n\nDrop raw ideas, questions, and source links here.\n"),
createdFiles,
);
await writeFileIfMissing(
path.join(rootDir, ".openclaw-wiki", "state.json"),
rootDir,
".openclaw-wiki/state.json",
withTrailingNewline(
JSON.stringify(
{
@@ -141,7 +137,7 @@ export async function initializeMemoryWikiVault(
),
createdFiles,
);
await writeFileIfMissing(path.join(rootDir, ".openclaw-wiki", "log.jsonl"), "", createdFiles);
await writeFileIfMissing(rootDir, ".openclaw-wiki/log.jsonl", "", createdFiles);
if (createdDirectories.length > 0 || createdFiles.length > 0) {
await appendMemoryWikiLog(rootDir, {