Files
openclaw/src/infra/tmp-openclaw-dir.ts

159 lines
4.5 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
type ResolvePreferredOpenClawTmpDirOptions = {
accessSync?: (path: string, mode?: number) => void;
lstatSync?: (path: string) => {
isDirectory(): boolean;
isSymbolicLink(): boolean;
mode?: number;
uid?: number;
};
mkdirSync?: (path: string, opts: { recursive: boolean; mode?: number }) => void;
getuid?: () => number | undefined;
tmpdir?: () => string;
};
type MaybeNodeError = { code?: string };
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 accessSync = options.accessSync ?? fs.accessSync;
const lstatSync = options.lstatSync ?? fs.lstatSync;
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
const getuid =
options.getuid ??
(() => {
try {
return typeof process.getuid === "function" ? process.getuid() : undefined;
} catch {
return undefined;
}
});
const tmpdir = options.tmpdir ?? os.tmpdir;
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;
}
// Avoid group/other writable dirs when running on multi-user hosts.
if (typeof st.mode === "number" && (st.mode & 0o022) !== 0) {
return false;
}
return true;
};
const fallback = (): string => {
const base = tmpdir();
const suffix = uid === undefined ? "openclaw" : `openclaw-${uid}`;
return path.join(base, suffix);
};
const isTrustedPreferredDir = (st: {
isDirectory(): boolean;
isSymbolicLink(): boolean;
mode?: number;
uid?: number;
}): boolean => {
return st.isDirectory() && !st.isSymbolicLink() && isSecureDirForUser(st);
};
const resolvePreferredState = (
requireWritableAccess: boolean,
): "available" | "missing" | "invalid" => {
try {
const preferred = lstatSync(POSIX_OPENCLAW_TMP_DIR);
if (!isTrustedPreferredDir(preferred)) {
return "invalid";
}
if (requireWritableAccess) {
accessSync(POSIX_OPENCLAW_TMP_DIR, fs.constants.W_OK | fs.constants.X_OK);
}
return "available";
} catch (err) {
if (isNodeErrorWithCode(err, "ENOENT")) {
return "missing";
}
return "invalid";
}
};
const resolveFallbackState = (
fallbackPath: string,
requireWritableAccess: boolean,
): "available" | "missing" | "invalid" => {
try {
const candidate = lstatSync(fallbackPath);
if (!isTrustedPreferredDir(candidate)) {
return "invalid";
}
if (requireWritableAccess) {
accessSync(fallbackPath, fs.constants.W_OK | fs.constants.X_OK);
}
return "available";
} catch (err) {
if (isNodeErrorWithCode(err, "ENOENT")) {
return "missing";
}
return "invalid";
}
};
const ensureTrustedFallbackDir = (): string => {
const fallbackPath = fallback();
const state = resolveFallbackState(fallbackPath, true);
if (state === "available") {
return fallbackPath;
}
if (state === "invalid") {
throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
}
try {
mkdirSync(fallbackPath, { recursive: true, mode: 0o700 });
} catch {
throw new Error(`Unable to create fallback OpenClaw temp dir: ${fallbackPath}`);
}
if (resolveFallbackState(fallbackPath, true) !== "available") {
throw new Error(`Unsafe fallback OpenClaw temp dir: ${fallbackPath}`);
}
return fallbackPath;
};
const existingPreferredState = resolvePreferredState(true);
if (existingPreferredState === "available") {
return POSIX_OPENCLAW_TMP_DIR;
}
if (existingPreferredState === "invalid") {
return ensureTrustedFallbackDir();
}
try {
accessSync("/tmp", fs.constants.W_OK | fs.constants.X_OK);
// Create with a safe default; subsequent callers expect it exists.
mkdirSync(POSIX_OPENCLAW_TMP_DIR, { recursive: true, mode: 0o700 });
if (resolvePreferredState(true) !== "available") {
return ensureTrustedFallbackDir();
}
return POSIX_OPENCLAW_TMP_DIR;
} catch {
return ensureTrustedFallbackDir();
}
}