mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-15 02:40:43 +00:00
176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
import fs from "node:fs";
|
|
import { tmpdir as getOsTmpDir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
|
|
|
|
type MaybeNodeError = { code?: string };
|
|
|
|
type SecureDirStat = {
|
|
isDirectory(): boolean;
|
|
isSymbolicLink(): boolean;
|
|
mode?: number;
|
|
uid?: number;
|
|
};
|
|
|
|
export type ResolvePreferredOpenClawTmpDirOptions = {
|
|
accessSync?: (path: string, mode?: number) => void;
|
|
chmodSync?: (path: string, mode: number) => void;
|
|
getuid?: () => number | undefined;
|
|
lstatSync?: (path: string) => SecureDirStat;
|
|
mkdirSync?: (path: string, opts: { recursive: boolean; mode?: number }) => void;
|
|
platform?: NodeJS.Platform;
|
|
tmpdir?: () => string;
|
|
warn?: (message: string) => void;
|
|
};
|
|
|
|
function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError {
|
|
return (
|
|
typeof err === "object" &&
|
|
err !== null &&
|
|
"code" in err &&
|
|
(err as MaybeNodeError).code === code
|
|
);
|
|
}
|
|
|
|
export function resolvePreferredOpenClawTmpDir(
|
|
options: ResolvePreferredOpenClawTmpDirOptions = {},
|
|
): string {
|
|
const accessMode = fs.constants.W_OK | fs.constants.X_OK;
|
|
const accessSync = options.accessSync ?? fs.accessSync;
|
|
const chmodSync = options.chmodSync ?? fs.chmodSync;
|
|
const lstatSync = options.lstatSync ?? fs.lstatSync;
|
|
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
|
const warn = options.warn ?? ((message: string) => console.warn(message));
|
|
const getuid =
|
|
options.getuid ??
|
|
(() => {
|
|
try {
|
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
});
|
|
const tmpdir = typeof options.tmpdir === "function" ? options.tmpdir : getOsTmpDir;
|
|
const platform = options.platform ?? process.platform;
|
|
const uid = getuid();
|
|
|
|
const isSecureDirForUser = (st: { mode?: number; uid?: number }): boolean => {
|
|
if (uid === undefined) {
|
|
return true;
|
|
}
|
|
if (typeof st.uid === "number" && st.uid !== uid) {
|
|
return false;
|
|
}
|
|
return typeof st.mode !== "number" || (st.mode & 0o022) === 0;
|
|
};
|
|
|
|
const fallback = (): string => {
|
|
const suffix = uid === undefined ? "openclaw" : `openclaw-${uid}`;
|
|
const joiner = platform === "win32" ? path.win32.join : path.join;
|
|
return joiner(tmpdir(), suffix);
|
|
};
|
|
|
|
const isTrustedTmpDir = (st: SecureDirStat): boolean =>
|
|
st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st);
|
|
|
|
const resolveDirState = (candidatePath: string): "available" | "missing" | "invalid" => {
|
|
try {
|
|
const candidate = lstatSync(candidatePath);
|
|
if (!isTrustedTmpDir(candidate)) {
|
|
return "invalid";
|
|
}
|
|
accessSync(candidatePath, accessMode);
|
|
return "available";
|
|
} catch (err) {
|
|
return isNodeErrorWithCode(err, "ENOENT") ? "missing" : "invalid";
|
|
}
|
|
};
|
|
|
|
const tryRepairWritableBits = (candidatePath: string): boolean => {
|
|
try {
|
|
const st = lstatSync(candidatePath);
|
|
if (!st.isDirectory() || st.isSymbolicLink()) {
|
|
return false;
|
|
}
|
|
if (uid !== undefined && typeof st.uid === "number" && st.uid !== uid) {
|
|
return false;
|
|
}
|
|
if (typeof st.mode !== "number") {
|
|
return false;
|
|
}
|
|
if ((st.mode & 0o022) === 0) {
|
|
return resolveDirState(candidatePath) === "available";
|
|
}
|
|
try {
|
|
chmodSync(candidatePath, 0o700);
|
|
} catch (chmodErr) {
|
|
if (
|
|
isNodeErrorWithCode(chmodErr, "EPERM") ||
|
|
isNodeErrorWithCode(chmodErr, "EACCES") ||
|
|
isNodeErrorWithCode(chmodErr, "ENOENT")
|
|
) {
|
|
return resolveDirState(candidatePath) === "available";
|
|
}
|
|
throw chmodErr;
|
|
}
|
|
warn(`[openclaw] tightened permissions on temp dir: ${candidatePath}`);
|
|
return resolveDirState(candidatePath) === "available";
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const ensureTrustedFallbackDir = (): string => {
|
|
const fallbackPath = fallback();
|
|
const state = resolveDirState(fallbackPath);
|
|
if (state === "available") {
|
|
return fallbackPath;
|
|
}
|
|
if (state === "invalid") {
|
|
if (tryRepairWritableBits(fallbackPath)) {
|
|
return fallbackPath;
|
|
}
|
|
throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
|
|
}
|
|
try {
|
|
mkdirSync(fallbackPath, { recursive: true, mode: 0o700 });
|
|
chmodSync(fallbackPath, 0o700);
|
|
} catch {
|
|
throw new Error(`Unable to create fallback OpenClaw temp dir: ${fallbackPath}`);
|
|
}
|
|
if (resolveDirState(fallbackPath) !== "available" && !tryRepairWritableBits(fallbackPath)) {
|
|
throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
|
|
}
|
|
return fallbackPath;
|
|
};
|
|
|
|
if (platform === "win32") {
|
|
return ensureTrustedFallbackDir();
|
|
}
|
|
|
|
const preferredDir = POSIX_OPENCLAW_TMP_DIR;
|
|
const preferredState = resolveDirState(preferredDir);
|
|
if (preferredState === "available") {
|
|
return preferredDir;
|
|
}
|
|
if (preferredState === "invalid") {
|
|
if (tryRepairWritableBits(preferredDir)) {
|
|
return preferredDir;
|
|
}
|
|
return ensureTrustedFallbackDir();
|
|
}
|
|
|
|
try {
|
|
accessSync(path.dirname(preferredDir), accessMode);
|
|
mkdirSync(preferredDir, { recursive: true, mode: 0o700 });
|
|
chmodSync(preferredDir, 0o700);
|
|
if (resolveDirState(preferredDir) !== "available" && !tryRepairWritableBits(preferredDir)) {
|
|
return ensureTrustedFallbackDir();
|
|
}
|
|
return preferredDir;
|
|
} catch {
|
|
return ensureTrustedFallbackDir();
|
|
}
|
|
}
|