[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

@@ -40,6 +40,7 @@ import {
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
import {
refreshCodexAppServerAuthTokens,
@@ -444,7 +445,7 @@ export async function runCodexAppServerAttempt(
runId: params.runId,
},
});
const hadSessionFile = await fileExists(params.sessionFile);
const hadSessionFile = await pathExists(params.sessionFile);
let historyMessages = (await readMirroredSessionHistoryMessages(params.sessionFile)) ?? [];
const hookContext = {
runId: params.runId,
@@ -1927,18 +1928,6 @@ async function mirrorTranscriptBestEffort(params: {
}
}
async function fileExists(filePath: string): Promise<boolean> {
try {
await fs.stat(filePath);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return false;
}
throw error;
}
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}

View File

@@ -1,22 +1,9 @@
import { withTimeout as withSharedTimeout } from "openclaw/plugin-sdk/security-runtime";
export async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
timeoutMessage: string,
): Promise<T> {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
return await promise;
}
let timeout: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(timeoutMessage)), Math.max(1, timeoutMs));
}),
]);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
return await withSharedTimeout(promise, timeoutMs, { message: timeoutMessage });
}

View File

@@ -6,6 +6,10 @@ import type {
EmbeddedRunAttemptResult,
} from "openclaw/plugin-sdk/agent-harness";
import { resolveUserPath } from "openclaw/plugin-sdk/agent-harness";
import {
appendRegularFile,
resolveRegularFileAppendFlags,
} from "openclaw/plugin-sdk/security-runtime";
type CodexTrajectoryRecorder = {
filePath: string;
@@ -39,13 +43,7 @@ type CodexTrajectoryOpenFlagConstants = Pick<
export function resolveCodexTrajectoryAppendFlags(
constants: CodexTrajectoryOpenFlagConstants = nodeFs.constants,
): number {
const noFollow = constants.O_NOFOLLOW;
return (
constants.O_CREAT |
constants.O_APPEND |
constants.O_WRONLY |
(typeof noFollow === "number" ? noFollow : 0)
);
return resolveRegularFileAppendFlags(constants);
}
export function resolveCodexTrajectoryPointerFlags(
@@ -60,78 +58,13 @@ export function resolveCodexTrajectoryPointerFlags(
);
}
async function assertNoSymlinkParents(filePath: string): Promise<void> {
const resolvedDir = path.resolve(path.dirname(filePath));
const parsed = path.parse(resolvedDir);
const relativeParts = path.relative(parsed.root, resolvedDir).split(path.sep).filter(Boolean);
let current = parsed.root;
for (const part of relativeParts) {
current = path.join(current, part);
const stat = await fs.lstat(current);
if (stat.isSymbolicLink()) {
if (path.dirname(current) === parsed.root) {
continue;
}
throw new Error(`Refusing to write trajectory under symlinked directory: ${current}`);
}
if (!stat.isDirectory()) {
throw new Error(`Refusing to write trajectory under non-directory: ${current}`);
}
}
}
function verifyStableOpenedTrajectoryFile(params: {
preOpenStat?: nodeFs.Stats;
postOpenStat: nodeFs.Stats;
filePath: string;
}): void {
if (!params.postOpenStat.isFile()) {
throw new Error(`Refusing to write trajectory to non-file: ${params.filePath}`);
}
if (params.postOpenStat.nlink > 1) {
throw new Error(`Refusing to write trajectory to hardlinked file: ${params.filePath}`);
}
const pre = params.preOpenStat;
if (pre && (pre.dev !== params.postOpenStat.dev || pre.ino !== params.postOpenStat.ino)) {
throw new Error(`Refusing to write trajectory after file changed: ${params.filePath}`);
}
}
async function safeAppendTrajectoryFile(filePath: string, line: string): Promise<void> {
await assertNoSymlinkParents(filePath);
let preOpenStat: nodeFs.Stats | undefined;
try {
const stat = await fs.lstat(filePath);
if (stat.isSymbolicLink()) {
throw new Error(`Refusing to write trajectory through symlink: ${filePath}`);
}
if (!stat.isFile()) {
throw new Error(`Refusing to write trajectory to non-file: ${filePath}`);
}
preOpenStat = stat;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
throw err;
}
}
const lineBytes = Buffer.byteLength(line, "utf8");
if ((preOpenStat?.size ?? 0) + lineBytes > TRAJECTORY_RUNTIME_FILE_MAX_BYTES) {
return;
}
const handle = await fs.open(filePath, resolveCodexTrajectoryAppendFlags(), 0o600);
try {
const stat = await handle.stat();
verifyStableOpenedTrajectoryFile({ preOpenStat, postOpenStat: stat, filePath });
if (stat.size + lineBytes > TRAJECTORY_RUNTIME_FILE_MAX_BYTES) {
return;
}
await handle.chmod(0o600);
await handle.appendFile(line, "utf8");
} finally {
await handle.close();
}
await appendRegularFile({
filePath,
content: line,
maxFileBytes: TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
rejectSymlinkParents: true,
});
}
function boundedTrajectoryLine(event: Record<string, unknown>): string | undefined {

View File

@@ -1,14 +1,10 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
export async function exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
return await pathExists(filePath);
}
export async function isDirectory(filePath: string | undefined): Promise<boolean> {