mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user