docs: document root runtime guard scripts

This commit is contained in:
Peter Steinberger
2026-06-04 23:34:04 -04:00
parent fb750e6eed
commit 056421f4f8
14 changed files with 158 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Rejects local wildcard re-exports in guarded extension API barrels.
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -38,6 +39,9 @@ async function listGuardedFiles(rootDir = repoRoot) {
);
}
/**
* Finds local wildcard re-export lines in a barrel source string.
*/
export function findLocalWildcardReexports(source) {
return source
.split(/\r?\n/u)
@@ -45,6 +49,9 @@ export function findLocalWildcardReexports(source) {
.filter(({ text }) => LOCAL_WILDCARD_REEXPORT_PATTERN.test(text));
}
/**
* Collects guarded extension API/runtime barrels that use wildcard re-exports.
*/
export async function collectExtensionWildcardReexports(rootDir = repoRoot) {
const files = await listGuardedFiles(rootDir);
const violations = [];
@@ -61,6 +68,9 @@ export async function collectExtensionWildcardReexports(rootDir = repoRoot) {
return violations;
}
/**
* Runs the extension wildcard re-export guard.
*/
export async function main(argv = process.argv.slice(2), io = process) {
const json = argv.includes("--json");
const violations = await collectExtensionWildcardReexports();

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Runs gateway startup and QA scenarios while checking hot CPU observations.
import { spawnSync as defaultSpawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -350,6 +351,9 @@ async function main(params = {}) {
}
}
/**
* Test-only access to the gateway CPU scenario parser and runner helpers.
*/
export const testing = {
hasPrivateQaDist,
parseArgs,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Measures gateway watch idle CPU and dist/runtime artifact churn.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
@@ -44,10 +45,16 @@ const WATCH_GATEWAY_SKIP_ENV = {
NODE_ENV: "test",
};
/**
* Maximum retained stdout/stderr text for gateway watch diagnostics.
*/
export const WATCH_LOG_CAPTURE_MAX_CHARS = 2 * 1024 * 1024;
const WATCH_BUILD_DETECTION_MAX_CHARS = 4096;
const NON_NEGATIVE_INTEGER_PATTERN = /^(0|[1-9]\d*)$/u;
/**
* Appends watch output while preserving only the diagnostic tail.
*/
export function appendBoundedWatchLog(current, chunk, maxChars = WATCH_LOG_CAPTURE_MAX_CHARS) {
const next = `${current}${String(chunk)}`;
if (next.length <= maxChars) {
@@ -62,6 +69,9 @@ function formatCapturedWatchLog(text, truncated) {
: text;
}
/**
* Updates bounded watch-build detection state from new output.
*/
export function updateWatchBuildDetection(state, chunk) {
const combined = `${state.buffer ?? ""}${String(chunk)}`;
const next = appendBoundedWatchLog("", combined, WATCH_BUILD_DETECTION_MAX_CHARS);
@@ -74,6 +84,9 @@ export function updateWatchBuildDetection(state, chunk) {
};
}
/**
* Parses a safe non-negative integer CLI value.
*/
export function readNonNegativeInteger(value, label) {
const raw = String(value).trim();
if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) {
@@ -86,6 +99,9 @@ export function readNonNegativeInteger(value, label) {
return parsed;
}
/**
* Parses gateway watch regression CLI arguments.
*/
export function parseArgs(argv) {
const args = stripLeadingPackageManagerSeparator(argv);
const options = { ...DEFAULTS };
@@ -396,6 +412,9 @@ function readProcessTreeCpuMs(rootPid) {
return totalCpuMs;
}
/**
* Reports whether gateway watch output contains a ready marker.
*/
export function hasGatewayReadyLog(text) {
return /\[gateway\] (?:http server listening|ready \()/.test(text);
}
@@ -504,6 +523,9 @@ function parseTimingFile(timeFilePath) {
};
}
/**
* Runs a bounded gateway watch process and captures timing/log artifacts.
*/
export async function runTimedWatch(options, outputDir, deps = {}) {
const allocatePort = deps.allocateLoopbackPort ?? allocateLoopbackPort;
const parseTiming = deps.parseTimingFile ?? parseTimingFile;
@@ -642,6 +664,9 @@ export async function runTimedWatch(options, outputDir, deps = {}) {
}
}
/**
* Stops the timed watch child process with TERM/KILL fallback.
*/
export async function stopTimedWatchChild(child, watchPid, options, deps = {}) {
const killProcess = deps.killProcess ?? ((pid, signal) => process.kill(pid, signal));
const currentExit = () =>
@@ -749,6 +774,9 @@ function buildRunNodeDeps(env) {
};
}
/**
* Reports whether restored CI artifacts need fresh build stamps.
*/
export function shouldRefreshBuildStampForRestoredArtifacts(params) {
return (
params.skipBuild === true &&
@@ -757,6 +785,9 @@ export function shouldRefreshBuildStampForRestoredArtifacts(params) {
);
}
/**
* Writes build and runtime-postbuild stamps for the current artifact set.
*/
export function writeBuildAndRuntimePostBuildStamps(params = {}) {
const cwd = params.cwd ?? process.cwd();
writeBuildStamp({ cwd });

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Ensures ingress agent command callsites pass explicit owner context.
import path from "node:path";
import ts from "typescript";
import { bundledPluginFile } from "./lib/bundled-plugin-paths.mjs";
@@ -19,6 +20,9 @@ const enforcedFiles = new Set([
"src/gateway/server-node-events.ts",
]);
/**
* Finds legacy `agentCommand(...)` call lines in ingress-owned source.
*/
export function findLegacyAgentCommandCallLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return collectCallExpressionLines(ts, sourceFile, (node) => {
@@ -27,6 +31,9 @@ export function findLegacyAgentCommandCallLines(content, fileName = "source.ts")
});
}
/**
* Runs the ingress owner-context guard.
*/
export async function main() {
await runCallsiteGuard({
importMetaUrl: import.meta.url,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Enforces Kysely and SQLite guardrails in infrastructure code.
import { promises as fs } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
@@ -217,6 +218,9 @@ function isPersistedStringCastType(typeText) {
].some((pattern) => pattern.test(typeText));
}
/**
* Collects Kysely/raw SQLite violations from one source file.
*/
export function collectKyselyGuardrailViolations(content, relativePath) {
const sourceFile = ts.createSourceFile(relativePath, content, ts.ScriptTarget.Latest, true);
const imports = collectImports(sourceFile);
@@ -338,6 +342,9 @@ export function collectKyselyGuardrailViolations(content, relativePath) {
return violations;
}
/**
* Collects Kysely guardrail violations across configured source roots.
*/
export async function collectKyselyGuardrails() {
const files = await collectTypeScriptFilesFromRoots(sourceRoots, { includeTests: true });
const violations = [];
@@ -351,6 +358,9 @@ export async function collectKyselyGuardrails() {
return violations;
}
/**
* Runs the Kysely guardrail check.
*/
export async function main() {
const violations = await collectKyselyGuardrails();
if (violations.length === 0) {

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// Checks extension media downloads keep helper-read/save calls close together.
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Reproduces memory-search file descriptor retention with a synthetic workspace.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
@@ -26,8 +27,17 @@ const ISSUE_FILE_COUNTS = [
const ISSUE_MEMORY_FILE_COUNT = ISSUE_FILE_COUNTS.reduce((sum, [, count]) => sum + count, 0);
const DEFAULT_FILE_COUNT = 512;
const DEFAULT_MAX_WORKSPACE_REG_FDS = process.platform === "darwin" ? 8 : 64;
/**
* Maximum gateway-ready output tail retained while waiting for startup.
*/
export const GATEWAY_READY_OUTPUT_MAX_CHARS = 128 * 1024;
/**
* Maximum bytes read from the memory_search HTTP response.
*/
export const MEMORY_SEARCH_RESPONSE_MAX_BYTES = 256 * 1024;
/**
* Probe query expected to hit the synthetic top-level memory file.
*/
export const MEMORY_SEARCH_PROBE_QUERY = "Top-level memory file";
const SKIP_GATEWAY_ENV = {
@@ -88,6 +98,9 @@ function stripPackageManagerSeparatorForKnownFlags(argv) {
: argv;
}
/**
* Parses a safe non-negative integer option.
*/
export function readNumber(value, label) {
const raw = String(value).trim();
if (!NON_NEGATIVE_INTEGER_PATTERN.test(raw)) {
@@ -100,6 +113,9 @@ export function readNumber(value, label) {
return parsed;
}
/**
* Parses a safe positive integer option.
*/
export function readPositiveNumber(value, label) {
const parsed = readNumber(value, label);
if (parsed <= 0) {
@@ -118,6 +134,9 @@ function readPositiveNumberEnv(name, fallback) {
return raw == null || raw.trim() === "" ? fallback : readPositiveNumber(raw, name);
}
/**
* Parses memory FD repro CLI arguments and environment fallbacks.
*/
export function parseArgs(argv) {
const args = stripPackageManagerSeparatorForKnownFlags(argv);
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
@@ -278,6 +297,9 @@ function writeSyntheticWorkspace(workspaceDir, fileCount) {
}
}
/**
* Writes isolated OpenClaw config for the synthetic memory workspace.
*/
export function writeConfig({ homeDir, workspaceDir, port, token }) {
const configDir = path.join(homeDir, ".openclaw");
fs.mkdirSync(configDir, { recursive: true });
@@ -352,6 +374,9 @@ function preindexSyntheticMemory(env) {
logStep("preindex complete");
}
/**
* Updates bounded gateway-ready output state from a stdout/stderr chunk.
*/
export function updateGatewayReadyOutputState(
state,
chunk,
@@ -431,10 +456,16 @@ function sampleFds({ label, pid, workspaceRealPath }) {
return sample;
}
/**
* Reports whether a spawned child has already exited.
*/
export function hasChildExited(child) {
return child.exitCode !== null || child.signalCode !== null;
}
/**
* Waits until gateway output and listener state both indicate readiness.
*/
export async function waitForGatewayReady({ child, port, logPath, timeoutMs }) {
const startedAt = Date.now();
let outputState = { tail: "", readySeen: false };
@@ -458,6 +489,9 @@ export async function waitForGatewayReady({ child, port, logPath, timeoutMs }) {
throw new Error(`gateway did not become ready within ${timeoutMs}ms; see ${logPath}`);
}
/**
* Stops the gateway child using the default process/runtime hooks.
*/
export async function stopGateway({ child, port }) {
return stopGatewayWithRuntime({
child,
@@ -467,6 +501,9 @@ export async function stopGateway({ child, port }) {
});
}
/**
* Stops the gateway child and any remaining listener process.
*/
export async function stopGatewayWithRuntime({
child,
childExitPollIntervalMs = 100,
@@ -499,6 +536,9 @@ export async function stopGatewayWithRuntime({
}
}
/**
* Reads an HTTP response body up to a configured byte limit.
*/
export { readBoundedResponseText };
function signalChild(child, signal) {
@@ -549,6 +589,9 @@ function parseToolTextContent(result) {
return null;
}
/**
* Classifies the memory_search HTTP response into success/error details.
*/
export function classifyMemorySearchInvokeResponse({ httpOk, status, bodyText }) {
const parsedBody = parseJsonValue(bodyText);
const body = asRecord(parsedBody);

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Rejects unresolved merge conflict markers in tracked files.
import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
@@ -11,6 +12,9 @@ function isBinaryBuffer(buffer) {
return buffer.includes(0);
}
/**
* Returns one-based line numbers containing merge conflict markers.
*/
export function findConflictMarkerLines(content) {
const lines = content.split(/\r?\n/u);
const matches = [];
@@ -27,6 +31,9 @@ export function findConflictMarkerLines(content) {
return matches;
}
/**
* Lists tracked files in the repository.
*/
export function listTrackedFiles(cwd = process.cwd()) {
const output = execFileSync("git", ["ls-files", "-z"], {
cwd,
@@ -38,6 +45,9 @@ export function listTrackedFiles(cwd = process.cwd()) {
.map((relativePath) => path.join(cwd, relativePath));
}
/**
* Scans files for merge conflict markers, skipping binary content.
*/
export function findConflictMarkersInFiles(filePaths, readFile = fs.readFileSync) {
const violations = [];
for (const filePath of filePaths) {
@@ -64,6 +74,9 @@ export function findConflictMarkersInFiles(filePaths, readFile = fs.readFileSync
return violations;
}
/**
* Uses git grep to list tracked files that may contain conflict markers.
*/
export function listTrackedFilesWithConflictMarkerCandidates(cwd = process.cwd(), run = spawnSync) {
const result = run(
"git",
@@ -87,10 +100,16 @@ export function listTrackedFilesWithConflictMarkerCandidates(cwd = process.cwd()
.map((relativePath) => path.join(cwd, relativePath));
}
/**
* Finds merge conflict markers in tracked repository files.
*/
export function findConflictMarkersInTrackedFiles(cwd = process.cwd()) {
return findConflictMarkersInFiles(listTrackedFilesWithConflictMarkerCandidates(cwd));
}
/**
* Runs the merge conflict marker check.
*/
export async function main() {
const cwd = process.cwd();
const violations = findConflictMarkersInTrackedFiles(cwd);

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Prevents direct pairing-store group auth reads outside resolver helpers.
import ts from "typescript";
import { createPairingGuardContext } from "./lib/pairing-guard-context.mjs";
import {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Blocks host-random tmpdir usage in messaging/channel runtime sources.
import ts from "typescript";
import { bundledPluginFile } from "./lib/bundled-plugin-paths.mjs";
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
@@ -9,6 +10,9 @@ import {
unwrapExpression,
} from "./lib/ts-guard-utils.mjs";
/**
* Source roots scanned for unsafe messaging tmpdir usage.
*/
export const messagingTmpdirGuardSourceRoots = [
"src/channels",
"src/infra/outbound",
@@ -53,6 +57,9 @@ function collectOsTmpdirImports(sourceFile) {
return { osNamespaceOrDefault, namedTmpdir };
}
/**
* Finds `os.tmpdir()` or imported `tmpdir()` call lines in source.
*/
export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const { osNamespaceOrDefault, namedTmpdir } = collectOsTmpdirImports(sourceFile);
@@ -70,6 +77,9 @@ export function findMessagingTmpdirCallLines(content, fileName = "source.ts") {
});
}
/**
* Runs the messaging tmpdir guard.
*/
export async function main() {
await runCallsiteGuard({
importMetaUrl: import.meta.url,

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Blocks new raw fetch callsites in channel and plugin runtime sources.
import ts from "typescript";
import { bundledPluginCallsite } from "./lib/bundled-plugin-paths.mjs";
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
@@ -79,6 +80,9 @@ function isRawFetchCall(expression) {
return false;
}
/**
* Finds raw `fetch(...)` and `globalThis.fetch(...)` call lines.
*/
export function findRawFetchCallLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return collectCallExpressionLines(ts, sourceFile, (node) =>
@@ -86,6 +90,9 @@ export function findRawFetchCallLines(content, fileName = "source.ts") {
);
}
/**
* Runs the raw channel/plugin fetch guard.
*/
export async function main() {
await runCallsiteGuard({
importMetaUrl: import.meta.url,

View File

@@ -1,3 +1,4 @@
// Rejects raw Node http2 imports in source and extension code.
import fs from "node:fs";
import path from "node:path";
const SOURCE_ROOTS = ["src", "extensions"];

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Ensures UI code opens external URLs through the safe helper.
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
@@ -37,6 +38,9 @@ function isRawWindowOpenCall(expression) {
);
}
/**
* Finds raw `window.open(...)` or `globalThis.open(...)` call lines.
*/
export function findRawWindowOpenLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const lines = [];
@@ -52,6 +56,9 @@ export function findRawWindowOpenLines(content, fileName = "source.ts") {
return lines;
}
/**
* Runs the raw window.open guard.
*/
export async function main() {
const files = await collectTypeScriptFiles(uiSourceDir, {
extraTestSuffixes: [".browser.test.ts", ".node.test.ts"],

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Blocks deprecated plugin `registerHttpHandler` callsites.
import ts from "typescript";
import { runCallsiteGuard } from "./lib/callsite-guard.mjs";
import {
@@ -15,6 +16,9 @@ function isDeprecatedRegisterHttpHandlerCall(expression) {
return ts.isPropertyAccessExpression(callee) && callee.name.text === "registerHttpHandler";
}
/**
* Finds deprecated `registerHttpHandler(...)` call lines.
*/
export function findDeprecatedRegisterHttpHandlerLines(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
return collectCallExpressionLines(ts, sourceFile, (node) =>
@@ -22,6 +26,9 @@ export function findDeprecatedRegisterHttpHandlerLines(content, fileName = "sour
);
}
/**
* Runs the deprecated HTTP handler API guard.
*/
export async function main() {
await runCallsiteGuard({
importMetaUrl: import.meta.url,