mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 07:13:47 +02:00
* refactor(qa): add shared transport contract and suite migration * refactor(qa): harden worker gateway startup * fix(qa): scope waits and sanitize shutdown artifacts * fix(qa): confine artifacts and redact preserved logs * fix(qa): block symlink escapes in artifact paths * fix(gateway): clear shutdown race timers * fix(qa): harden shutdown cleanup paths * fix(qa): sanitize gateway logs in thrown errors * fix(qa): harden suite startup and artifact paths * fix(qa): stage bundled plugins from mutated config * fix(qa): broaden gateway log bearer redaction * fix(qa-channel): restore runtime export * fix(qa): stop failed gateway startups as a process tree * fix(qa-channel): load runtime hook from api surface
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
export function resolveRepoRelativeOutputDir(repoRoot: string, outputDir?: string) {
|
|
if (!outputDir) {
|
|
return undefined;
|
|
}
|
|
if (path.isAbsolute(outputDir)) {
|
|
throw new Error("--output-dir must be a relative path inside the repo root.");
|
|
}
|
|
const resolved = path.resolve(repoRoot, outputDir);
|
|
const relative = path.relative(repoRoot, resolved);
|
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
throw new Error("--output-dir must stay within the repo root.");
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
async function resolveNearestExistingPath(targetPath: string) {
|
|
let current = path.resolve(targetPath);
|
|
while (true) {
|
|
try {
|
|
await fs.lstat(current);
|
|
return current;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) {
|
|
throw new Error(`failed to resolve existing path for ${targetPath}`);
|
|
}
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
function assertRepoRelativePath(repoRoot: string, targetPath: string, label: string) {
|
|
const relative = path.relative(repoRoot, targetPath);
|
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
throw new Error(`${label} must stay within the repo root.`);
|
|
}
|
|
return relative;
|
|
}
|
|
|
|
async function assertNoSymlinkSegments(repoRoot: string, targetPath: string, label: string) {
|
|
const relative = assertRepoRelativePath(repoRoot, targetPath, label);
|
|
let current = repoRoot;
|
|
for (const segment of relative.split(path.sep).filter((entry) => entry.length > 0)) {
|
|
current = path.join(current, segment);
|
|
let stats: Awaited<ReturnType<typeof fs.lstat>> | null = null;
|
|
try {
|
|
stats = await fs.lstat(current);
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
break;
|
|
}
|
|
throw error;
|
|
}
|
|
if (stats.isSymbolicLink()) {
|
|
throw new Error(`${label} must not traverse symlinks.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function assertRepoBoundPath(repoRoot: string, targetPath: string, label: string) {
|
|
const repoRootResolved = path.resolve(repoRoot);
|
|
const targetResolved = path.resolve(targetPath);
|
|
assertRepoRelativePath(repoRootResolved, targetResolved, label);
|
|
await assertNoSymlinkSegments(repoRootResolved, targetResolved, label);
|
|
const repoRootReal = await fs.realpath(repoRootResolved);
|
|
const nearestExistingPath = await resolveNearestExistingPath(targetResolved);
|
|
const nearestExistingReal = await fs.realpath(nearestExistingPath);
|
|
assertRepoRelativePath(repoRootReal, nearestExistingReal, label);
|
|
return targetResolved;
|
|
}
|
|
|
|
export async function ensureRepoBoundDirectory(
|
|
repoRoot: string,
|
|
targetDir: string,
|
|
label: string,
|
|
opts?: { mode?: number },
|
|
) {
|
|
const repoRootResolved = path.resolve(repoRoot);
|
|
const targetResolved = path.resolve(targetDir);
|
|
const relative = assertRepoRelativePath(repoRootResolved, targetResolved, label);
|
|
const repoRootReal = await fs.realpath(repoRootResolved);
|
|
let current = repoRootResolved;
|
|
for (const segment of relative.split(path.sep).filter((entry) => entry.length > 0)) {
|
|
current = path.join(current, segment);
|
|
while (true) {
|
|
try {
|
|
const stats = await fs.lstat(current);
|
|
if (stats.isSymbolicLink()) {
|
|
throw new Error(`${label} must not traverse symlinks.`);
|
|
}
|
|
if (!stats.isDirectory()) {
|
|
throw new Error(`${label} must point to a directory.`);
|
|
}
|
|
break;
|
|
} catch (error) {
|
|
const code = (error as NodeJS.ErrnoException).code;
|
|
if (code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
try {
|
|
await fs.mkdir(current, { recursive: false, mode: opts?.mode });
|
|
} catch (mkdirError) {
|
|
if ((mkdirError as NodeJS.ErrnoException).code === "EEXIST") {
|
|
continue;
|
|
}
|
|
throw mkdirError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const targetReal = await fs.realpath(targetResolved);
|
|
assertRepoRelativePath(repoRootReal, targetReal, label);
|
|
return targetResolved;
|
|
}
|