openclaw/scripts/check-temp-path-guardrails.ts
Peter Steinberger afcb342244
refactor(scripts): deslop tooling scripts third pass (#161354)
* refactor(scripts): deslop tooling scripts third pass

Consolidate AST traversal, binding and shadow handling, report grouping,
fixture metadata, and developer harness plumbing. Remove the unused PR
comment verdict parser and one-use forwarding layers while preserving CLI,
guard, baseline, and generated-byte contracts.

* test(scripts): parse mocked fetch requests with Request

* test(scripts): include shared AST helper in protocol fixture
2026-09-29 17:00:46 -07:00

240 lines
6.8 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import pMap, { pMapSkip } from "p-map";
import { listRepoFilesSync } from "./check-file-utils.js";
type QuoteChar = "'" | '"' | "`";
type QuoteScanState = {
quote: QuoteChar | null;
escaped: boolean;
};
type RuntimeSourceGuardrailFile = {
relativePath: string;
source: string;
};
const WEAK_RANDOM_SAME_LINE_PATTERN =
/(?:Date\.now[^\r\n]*Math\.random|Math\.random[^\r\n]*Date\.now)/u;
const PATH_JOIN_CALL_PATTERN = /path\s*\.\s*join\s*\(/u;
const OS_TMPDIR_CALL_PATTERN = /os\s*\.\s*tmpdir\s*\(/u;
const FILE_READ_CONCURRENCY = 24;
const DEFAULT_GUARDRAIL_SKIP_PATTERNS = [
/\.test\.tsx?$/,
/\.test-helpers\.tsx?$/,
/\.test-utils\.tsx?$/,
/\.test-harness\.tsx?$/,
/\.test-support\.tsx?$/,
/\.suite\.tsx?$/,
/\.e2e\.tsx?$/,
/\.d\.ts$/,
/[\\/](?:__tests__|tests|test-helpers|test-utils|test-support)[\\/]/,
/[\\/][^\\/]*test-helpers(?:\.[^\\/]+)?\.ts$/,
/[\\/][^\\/]*test-utils(?:\.[^\\/]+)?\.ts$/,
/[\\/][^\\/]*test-harness(?:\.[^\\/]+)?\.ts$/,
/[\\/][^\\/]*test-support(?:\.[^\\/]+)?\.ts$/,
];
function shouldSkipGuardrailRuntimeSource(relativePath: string): boolean {
return DEFAULT_GUARDRAIL_SKIP_PATTERNS.some((pattern) => pattern.test(relativePath));
}
function stripCommentsForScan(input: string): string {
return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
}
function consumeQuotedChar(state: QuoteScanState, ch: string): boolean {
if (!state.quote) {
if (ch === "'" || ch === '"' || ch === "`") {
state.quote = ch;
}
return state.quote !== null;
}
if (state.escaped) {
state.escaped = false;
return true;
}
if (ch === "\\") {
state.escaped = true;
return true;
}
if (ch === state.quote) {
state.quote = null;
}
return true;
}
function findMatchingParen(source: string, openIndex: number): number {
let depth = 1;
const quoteState: QuoteScanState = { quote: null, escaped: false };
for (let i = openIndex + 1; i < source.length; i += 1) {
const ch = source.charAt(i);
if (consumeQuotedChar(quoteState, ch)) {
continue;
}
if (ch === "(") {
depth += 1;
continue;
}
if (ch === ")") {
depth -= 1;
if (depth === 0) {
return i;
}
}
}
return -1;
}
function splitTopLevelArguments(source: string): string[] {
const out: string[] = [];
let current = "";
let parenDepth = 0;
let bracketDepth = 0;
let braceDepth = 0;
const quoteState: QuoteScanState = { quote: null, escaped: false };
for (const ch of source) {
if (consumeQuotedChar(quoteState, ch)) {
current += ch;
continue;
}
if (ch === "(") {
parenDepth += 1;
} else if (ch === ")") {
parenDepth = Math.max(0, parenDepth - 1);
} else if (ch === "[") {
bracketDepth += 1;
} else if (ch === "]") {
bracketDepth = Math.max(0, bracketDepth - 1);
} else if (ch === "{") {
braceDepth += 1;
} else if (ch === "}") {
braceDepth = Math.max(0, braceDepth - 1);
} else if (ch === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
out.push(current.trim());
current = "";
continue;
}
current += ch;
}
if (current.trim()) {
out.push(current.trim());
}
return out;
}
function isOsTmpdirExpression(argument: string): boolean {
return /^os\s*\.\s*tmpdir\s*\(\s*\)$/u.test(argument.trim());
}
function mightContainDynamicTmpdirJoin(source: string): boolean {
if (!source.includes("path") || !source.includes("join") || !source.includes("tmpdir")) {
return false;
}
return (
(source.includes("path.join") || PATH_JOIN_CALL_PATTERN.test(source)) &&
(source.includes("os.tmpdir") || OS_TMPDIR_CALL_PATTERN.test(source)) &&
source.includes("`") &&
source.includes("${")
);
}
function hasDynamicTmpdirJoin(source: string): boolean {
if (!mightContainDynamicTmpdirJoin(source)) {
return false;
}
const scanSource = stripCommentsForScan(source);
const joinPattern = /path\s*\.\s*join\s*\(/gu;
let match: RegExpExecArray | null = joinPattern.exec(scanSource);
while (match) {
const openParenIndex = scanSource.indexOf("(", match.index);
if (openParenIndex !== -1) {
const closeParenIndex = findMatchingParen(scanSource, openParenIndex);
if (closeParenIndex !== -1) {
const argsSource = scanSource.slice(openParenIndex + 1, closeParenIndex);
const args = splitTopLevelArguments(argsSource);
const firstArg = args[0];
if (firstArg && isOsTmpdirExpression(firstArg)) {
for (const arg of args.slice(1)) {
const trimmed = arg.trim();
if (trimmed.startsWith("`") && trimmed.includes("${")) {
return true;
}
}
}
}
}
match = joinPattern.exec(scanSource);
}
return false;
}
function listTrackedRuntimeSourceFiles(repoRoot: string): string[] {
return listRepoFilesSync(repoRoot, {
roots: ["src", "extensions"],
includeFile: (relativePath) =>
(relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) &&
!shouldSkipGuardrailRuntimeSource(relativePath),
}).map((relativePath) => path.join(repoRoot, relativePath));
}
async function readRuntimeSourceFiles(
repoRoot: string,
absolutePaths: string[],
): Promise<RuntimeSourceGuardrailFile[]> {
return await pMap(
absolutePaths,
async (absolutePath) => {
try {
return {
relativePath: path.relative(repoRoot, absolutePath),
source: await fs.readFile(absolutePath, "utf8"),
};
} catch {
// File tracked by git but deleted on disk (e.g. pending deletion).
return pMapSkip;
}
},
{ concurrency: FILE_READ_CONCURRENCY, stopOnError: false },
);
}
async function main() {
const repoRoot = process.cwd();
const files = await readRuntimeSourceFiles(repoRoot, listTrackedRuntimeSourceFiles(repoRoot));
const offenders: string[] = [];
const weakRandomMatches: string[] = [];
for (const file of files) {
const source = file.source;
const mightContainWeakRandom = source.includes("Date.now") && source.includes("Math.random");
if (hasDynamicTmpdirJoin(source)) {
offenders.push(file.relativePath);
}
if (mightContainWeakRandom && WEAK_RANDOM_SAME_LINE_PATTERN.test(source)) {
weakRandomMatches.push(file.relativePath);
}
}
if (offenders.length === 0 && weakRandomMatches.length === 0) {
return;
}
if (offenders.length > 0) {
console.error("Dynamic os.tmpdir()/path.join() template paths found:");
for (const offender of offenders) {
console.error(`- ${offender}`);
}
}
if (weakRandomMatches.length > 0) {
console.error("Weak Date.now()+Math.random() same-line IDs found:");
for (const offender of weakRandomMatches) {
console.error(`- ${offender}`);
}
}
process.exitCode = 1;
}
await main();