openclaw/scripts/check-cli-bootstrap-imports.mts
Peter Steinberger 41bbe9b8df
fix(workers): ship Windows permission helper in portable archives (#174362)
fs-safe resolves its Windows permission helper beside the bundled module.
The portable worker manifest shipped only JavaScript, leaving four required
executable edges missing. Emit the pinned helper as an owned asset and carry
it through existing staging, regular-file checks and content hashes.

Follow executable URL edges in the package closure guard. Retain the lazy
native-disabled worker contract and reject unowned executables. Preserve all
authorization persistence and quarantine-refusal assertions while exposing
captured runner errors and the authorization commit milestone.

The new closure test failed on main; the real relocated archive then exposed
four missing helper edges. Repaired worker suite: 17/17. Package/bootstrap
checks: 37/37; bundle/archive suites: 35/35; bootstrap suite: 34/34. Full build,
scoped core/scripts/root types, lint and P2 review passed. Native Windows is
not available locally: this fixes the proven missing asset, without claiming
the original null-status timeout is conclusively explained.
2026-10-16 15:30:32 +00:00

583 lines
20 KiB
TypeScript

#!/usr/bin/env node
// Checks CLI bootstrap chunks for forbidden eager imports and size regressions.
import fs from "node:fs";
import module from "node:module";
import path from "node:path";
import type { Node as AcornNode } from "acorn";
import {
WORKER_BUNDLE_ARTIFACT_PATHS,
WORKER_BUNDLE_CHUNK_PATH_PATTERN,
} from "../src/shared/worker-bundle-hash.js";
import { reportLimitViolations, type LimitViolation } from "./lib/check-limits.mts";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { readGatewayRunChunks } from "./lib/gateway-run-chunk-metadata.mts";
import { visitJavaScriptStatements } from "./lib/javascript-statements.mjs";
import { isUnstagedWorkerDeployRuntimeArtifact } from "./lib/worker-deploy-build-plugin.mts";
const DEFAULT_ENTRYPOINTS = ["dist/entry.js", "dist/cli/run-main.js"];
const DEFAULT_NATIVE_HOOK_RELAY_ENTRYPOINT = "dist/native-hook-relay/entry.js";
const DEFAULT_NATIVE_HOOK_RELAY_STATIC_MAX_BYTES = 512 * 1024;
const NATIVE_HOOK_RELAY_FORBIDDEN_STATIC_MARKERS = [
"MAX_NATIVE_HOOK_RELAY_INVOCATIONS",
"getActivePluginSessionExtensionRegistry",
"requestDeferredPluginToolApproval",
"runBeforeToolCallHook",
];
// fs-safe must retain its package scope for optional native-platform loading.
const NATIVE_HOOK_RELAY_ALLOWED_EXTERNAL_IMPORTS = ["kysely", "@openclaw/fs-safe"];
// These import-safe subpaths retain proc-safe's package-scoped, lazy native loader.
const NATIVE_HOOK_RELAY_ALLOWED_EXTERNAL_SUBPATHS = new Set([
"@openclaw/proc-safe/errors",
"@openclaw/proc-safe/identity",
]);
const WORKER_DEPLOY_ENTRYPOINTS = WORKER_BUNDLE_ARTIFACT_PATHS.map(
(entry) => `dist/worker/${entry}`,
);
const DEFAULT_GATEWAY_RUN_CHUNK_MAX_BYTES = 70 * 1024;
const GATEWAY_RUN_CHUNK_MARKER_SETS = [
["const GATEWAY_AUTH_MODES", "function addGatewayRunCommand"],
["const GATEWAY_RUN_VALUE_KEYS", "function addGatewayRunCommand"],
];
const GATEWAY_RUN_FORBIDDEN_STATIC_IMPORTS = [
"control-ui-assets",
"diagnostic-stability-bundle",
"onboard-helpers",
"process-respawn",
"restart-sentinel",
"server-close",
"server-reload-hot",
"server-reload-managed",
];
const STATIC_IMPORT_RE =
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<specifier>[^"']+)["']/gu;
type CliBootstrapCheckParams = {
rootDir?: string;
entrypoints?: string[];
workerDeployEntrypoints?: readonly string[];
distDir?: string;
gatewayRunChunkMaxBytes?: number;
legacyGatewayChunkDiscovery?: boolean;
nativeHookRelayEntrypoint?: string;
requireNativeHookRelay?: boolean;
nativeHookRelayStaticMaxBytes?: number;
fs?: typeof fs;
logger?: { error(message: string): void };
};
function isBuiltinSpecifier(specifier: string) {
return specifier.startsWith("node:") || module.isBuiltin(specifier);
}
function isRelativeSpecifier(specifier: string) {
return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
}
function resolveRelativeImport(importer: string, specifier: string, fsImpl: typeof fs = fs) {
const base = specifier.startsWith("/")
? specifier
: path.resolve(path.dirname(importer), specifier);
const candidates = [
base,
`${base}.js`,
`${base}.mjs`,
`${base}.cjs`,
path.join(base, "index.js"),
path.join(base, "index.mjs"),
path.join(base, "index.cjs"),
];
return candidates.find((candidate) => {
try {
return fsImpl.statSync(candidate).isFile();
} catch {
return false;
}
});
}
/**
* Lists static import/export specifiers from a JavaScript source string.
*/
function listStaticImportSpecifiers(source: string) {
return [...source.matchAll(STATIC_IMPORT_RE)].map((match) => match.groups?.specifier ?? "");
}
function literalString(value: unknown): string | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return undefined;
}
const node = value as { type?: unknown; value?: unknown };
return node.type === "Literal" && typeof node.value === "string" ? node.value : undefined;
}
function isRequireCallName(value: unknown): boolean {
return typeof value === "string" && /^(?:require|_*require\d*)$/u.test(value);
}
function isRequireLikeCallee(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const node = value as Record<string, unknown>;
if (node.type === "Identifier") {
return isRequireCallName(node.name);
}
if (node.type !== "CallExpression") {
return false;
}
const callee = node.callee;
if (!callee || typeof callee !== "object" || Array.isArray(callee)) {
return false;
}
const member = callee as Record<string, unknown>;
if (member.type === "Identifier" && member.name === "createRequire") {
return true;
}
const property = member.property;
return (
member.type === "MemberExpression" &&
property !== null &&
typeof property === "object" &&
!Array.isArray(property) &&
(property as Record<string, unknown>).type === "Identifier" &&
(property as Record<string, unknown>).name === "createRequire"
);
}
function listRuntimeImportSpecifiers(source: string): string[] {
const specifiers: string[] = [];
visitJavaScriptStatements(source, { sourceType: "module", allowHashBang: true }, (statements) => {
const stack: unknown[] = statements;
while (stack.length > 0) {
const value = stack.pop();
if (!value || typeof value !== "object") {
continue;
}
if (Array.isArray(value)) {
stack.push(...value);
continue;
}
const node = value as AcornNode & Record<string, unknown>;
if (
node.type === "ImportDeclaration" ||
node.type === "ExportNamedDeclaration" ||
node.type === "ExportAllDeclaration" ||
node.type === "ImportExpression"
) {
const specifier = literalString(node.source);
if (specifier) {
specifiers.push(specifier);
}
} else if (node.type === "CallExpression") {
const callee = node.callee;
const args = node.arguments;
if (isRequireLikeCallee(callee) && Array.isArray(args)) {
const specifier = literalString(args[0]);
if (specifier) {
specifiers.push(specifier);
}
}
}
// Large worker bundles contain millions of nodes; avoid a pair allocation per property.
for (const key of Object.keys(node)) {
const child = node[key];
if (key === "start" || key === "end" || key === "loc" || key === "range") {
continue;
}
if (child && typeof child === "object") {
stack.push(child);
}
}
}
});
return [...new Set(specifiers)].toSorted((left, right) => left.localeCompare(right));
}
function walkStaticImportGraph(
fsImpl: typeof fs,
rootDir: string,
roots: string[],
onExternalSpecifier?: (filePath: string, specifier: string) => string,
onRelativeSpecifier?: (
filePath: string,
resolved: string,
specifier: string,
) => string | undefined,
onSource?: (filePath: string, source: string) => string[],
) {
const queue = roots.map((entrypoint) => path.resolve(rootDir, entrypoint));
const visited = new Set<string>();
const errors = [];
for (const filePath of queue) {
if (!filePath || visited.has(filePath)) {
continue;
}
visited.add(filePath);
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
errors.push(
`CLI bootstrap import guard could not read ${path.relative(rootDir, filePath) || filePath}. Run pnpm build first.`,
);
continue;
}
errors.push(...(onSource?.(filePath, source) ?? []));
for (const specifier of listStaticImportSpecifiers(source)) {
if (!specifier || isBuiltinSpecifier(specifier)) {
continue;
}
if (!isRelativeSpecifier(specifier)) {
const error = onExternalSpecifier?.(filePath, specifier);
if (error) {
errors.push(error);
}
continue;
}
const resolved = resolveRelativeImport(filePath, specifier, fsImpl);
if (!resolved) {
errors.push(
`CLI bootstrap import guard could not resolve "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
continue;
}
const error = onRelativeSpecifier?.(filePath, resolved, specifier);
if (error) {
errors.push(error);
}
if (!visited.has(resolved)) {
queue.push(resolved);
}
}
}
return errors;
}
/** Collects isolation and static-graph budget errors for the native hook relay executable. */
export function collectNativeHookRelayBundleErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const entrypoint = params.nativeHookRelayEntrypoint ?? DEFAULT_NATIVE_HOOK_RELAY_ENTRYPOINT;
const entrypointPath = path.resolve(rootDir, entrypoint);
// Release tooling also validates older packages whose supported relay is the general CLI.
// Current builds require this artifact; every present relay is checked in either mode.
if (!params.requireNativeHookRelay && !fsImpl.existsSync(entrypointPath)) {
return [];
}
const bundleDir = path.resolve(rootDir, params.distDir ?? "dist");
const maxBytes =
params.nativeHookRelayStaticMaxBytes ?? DEFAULT_NATIVE_HOOK_RELAY_STATIC_MAX_BYTES;
let staticBytes = 0;
const errors: Array<string | LimitViolation> = walkStaticImportGraph(
fsImpl,
rootDir,
[entrypoint],
(filePath, specifier) =>
NATIVE_HOOK_RELAY_ALLOWED_EXTERNAL_IMPORTS.some(
(dependency) => specifier === dependency || specifier.startsWith(`${dependency}/`),
) || NATIVE_HOOK_RELAY_ALLOWED_EXTERNAL_SUBPATHS.has(specifier)
? ""
: `Native hook relay static graph imports unexpected package "${specifier}" from ${
path.relative(rootDir, filePath) || filePath
}.`,
(filePath, resolved, specifier) => {
const relativeToBundle = path.relative(bundleDir, resolved);
return !relativeToBundle.startsWith("..") && !path.isAbsolute(relativeToBundle)
? undefined
: `Native hook relay static graph escapes the built runtime via "${specifier}" from ${
path.relative(rootDir, filePath) || filePath
}.`;
},
(filePath, source) => {
try {
staticBytes += fsImpl.statSync(filePath).size;
} catch {
staticBytes += Buffer.byteLength(source, "utf8");
}
return NATIVE_HOOK_RELAY_FORBIDDEN_STATIC_MARKERS.flatMap((marker) =>
source.includes(marker)
? [
`Native hook relay static graph contains server marker "${marker}" in ${
path.relative(rootDir, filePath) || filePath
}.`,
]
: [],
);
},
).filter(Boolean);
if (staticBytes > maxBytes) {
errors.push({
file: entrypoint,
title: "Native hook relay bundle budget",
message: `Native hook relay static graph is ${staticBytes} bytes, above budget ${maxBytes} bytes.`,
});
}
return errors.toSorted((left, right) =>
(typeof left === "string" ? left : left.message).localeCompare(
typeof right === "string" ? right : right.message,
),
);
}
/**
* Collects forbidden external import errors for CLI bootstrap entrypoints.
*/
export function collectCliBootstrapExternalImportErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const entrypoints = params.entrypoints ?? DEFAULT_ENTRYPOINTS;
const fsImpl = params.fs ?? fs;
const errors = walkStaticImportGraph(
fsImpl,
rootDir,
entrypoints,
(filePath, specifier) =>
`CLI bootstrap static graph imports external package "${specifier}" from ${path.relative(
rootDir,
filePath,
)}.`,
);
return errors.toSorted((left, right) => left.localeCompare(right));
}
function listJsFiles(dirPath: string, fsImpl: typeof fs = fs): string[] {
let entries;
try {
entries = fsImpl.readdirSync(dirPath, { withFileTypes: true });
} catch {
return [];
}
const files = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
files.push(...listJsFiles(fullPath, fsImpl));
continue;
}
if (entry.isFile() && /\.m?js$/u.test(entry.name)) {
files.push(fullPath);
}
}
return files;
}
/**
* Collects gateway-run chunk budget errors from built CLI output.
*/
export function collectGatewayRunChunkBudgetErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const distDir = path.resolve(rootDir, params.distDir ?? "dist");
const maxBytes = params.gatewayRunChunkMaxBytes ?? DEFAULT_GATEWAY_RUN_CHUNK_MAX_BYTES;
let chunks: Array<{ filePath: string; source: string }> = [];
if (params.legacyGatewayChunkDiscovery) {
// Current release tooling also qualifies frozen targets predating build-owned locators.
// Only that explicit caller may retain the historical full-tree discovery contract.
for (const filePath of listJsFiles(distDir, fsImpl)) {
let source;
try {
source = fsImpl.readFileSync(filePath, "utf8");
} catch {
continue;
}
if (
GATEWAY_RUN_CHUNK_MARKER_SETS.some((markers) =>
markers.every((marker) => source.includes(marker)),
)
) {
chunks.push({ filePath, source });
}
}
} else {
try {
chunks = readGatewayRunChunks(distDir, fsImpl);
} catch (error) {
return [
`CLI bootstrap import guard could not read gateway run chunk metadata: ${error instanceof Error ? error.message : String(error)}. Run pnpm build first.`,
];
}
}
if (chunks.length === 0) {
return [
"CLI bootstrap import guard could not find the bundled gateway run chunk. Run pnpm build first.",
];
}
const errors: Array<string | LimitViolation> = [];
for (const { filePath, source } of chunks) {
const relativePath = path.relative(rootDir, filePath) || filePath;
let size = Buffer.byteLength(source, "utf8");
try {
size = fsImpl.statSync(filePath).size;
} catch {
// Fall back to source byte length for in-memory test fixtures.
}
if (size > maxBytes) {
errors.push({
file: relativePath,
title: "Gateway run chunk budget",
message: `Gateway run chunk ${relativePath} is ${size} bytes, above budget ${maxBytes} bytes.`,
});
}
errors.push(
...walkStaticImportGraph(
fsImpl,
rootDir,
[filePath],
undefined,
(importerPath, resolved, specifier) => {
const resolvedRelativePath = path.relative(rootDir, resolved) || resolved;
const coldPath = [specifier, resolvedRelativePath].find((candidate) =>
GATEWAY_RUN_FORBIDDEN_STATIC_IMPORTS.some((forbidden) => candidate.includes(forbidden)),
);
return coldPath
? `Gateway run chunk ${relativePath} static graph imports cold path "${coldPath}" from ${
path.relative(rootDir, importerPath) || importerPath
}.`
: undefined;
},
),
);
}
return errors.toSorted((left, right) =>
(typeof left === "string" ? left : left.message).localeCompare(
typeof right === "string" ? right : right.message,
),
);
}
/** Collects closure and layout errors for the standalone worker deploy artifact. */
export function collectWorkerDeployArtifactErrors(params: CliBootstrapCheckParams = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
const entrypoints = (params.workerDeployEntrypoints ?? WORKER_DEPLOY_ENTRYPOINTS).map(
(entrypoint) => path.resolve(rootDir, entrypoint),
);
const artifactDir = path.resolve(rootDir, "dist/worker");
const artifactNames = new Set(
entrypoints.flatMap((entrypoint) => {
const name = path.basename(entrypoint);
return [name, `${name}.map`];
}),
);
const errors: string[] = [];
const sources: Array<{ relativeEntrypoint: string; source: string }> = [];
try {
for (const entry of fsImpl.readdirSync(artifactDir, { withFileTypes: true })) {
if (artifactNames.has(entry.name)) {
continue;
}
if (WORKER_BUNDLE_CHUNK_PATH_PATTERN.test(entry.name)) {
artifactNames.add(entry.name);
entrypoints.push(path.join(artifactDir, entry.name));
} else if (entry.name === "package.json") {
errors.push(
"Worker deploy artifact must not contain a dependency manifest or lifecycle scripts.",
);
} else if (entry.name === "node_modules") {
errors.push("Worker deploy artifact must not contain materialized dependencies.");
} else if (isUnstagedWorkerDeployRuntimeArtifact(entry.name, artifactNames)) {
errors.push(
`Worker deploy artifact emits unstaged runtime asset ${path.relative(
rootDir,
path.join(artifactDir, entry.name),
)}.`,
);
}
}
} catch (error) {
if (entrypoints.length === 0 && (error as NodeJS.ErrnoException).code === "ENOENT") {
return [];
}
errors.push(
`Worker deploy artifact directory ${path.relative(rootDir, artifactDir)} is unreadable.`,
);
}
for (const entrypoint of entrypoints) {
const relativeEntrypoint = path.relative(rootDir, entrypoint) || entrypoint;
try {
const stats = fsImpl.lstatSync(entrypoint);
if (stats.isSymbolicLink() || !stats.isFile()) {
return [`Worker deploy artifact ${relativeEntrypoint} must be a regular file.`];
}
if (entrypoint.endsWith(".mjs")) {
sources.push({
relativeEntrypoint,
source: fsImpl.readFileSync(entrypoint, "utf8"),
});
}
} catch {
return [`Worker deploy artifact ${relativeEntrypoint} is missing. Run pnpm build first.`];
}
}
for (const { relativeEntrypoint, source } of sources) {
try {
for (const specifier of listRuntimeImportSpecifiers(source)) {
if (
isBuiltinSpecifier(specifier) ||
specifier === "bun:ffi" ||
(specifier.startsWith("./") &&
specifier.endsWith(".mjs") &&
artifactNames.has(specifier.slice(2)))
) {
continue;
}
errors.push(
`Worker deploy artifact ${relativeEntrypoint} retains runtime import "${specifier}" instead of bundling it.`,
);
}
} catch (error) {
errors.push(
`Worker deploy artifact ${relativeEntrypoint} is not parseable JavaScript: ${
error instanceof Error ? error.message : String(error)
}.`,
);
}
}
return errors.toSorted((left, right) => left.localeCompare(right));
}
/**
* Runs the CLI bootstrap import, chunk-budget, and worker deploy checks.
*/
export function checkCliBootstrapExternalImports(params: CliBootstrapCheckParams = {}) {
const findings = [
...collectCliBootstrapExternalImportErrors(params),
...collectGatewayRunChunkBudgetErrors(params),
...collectNativeHookRelayBundleErrors(params),
...collectWorkerDeployArtifactErrors(params),
];
const errors = findings.filter((finding) => typeof finding === "string");
const limitsFailed = reportLimitViolations(
findings.filter((finding) => typeof finding !== "string"),
);
if (errors.length === 0 && !limitsFailed) {
return;
}
const logger = params.logger ?? console;
logger.error("CLI bootstrap import guard failed:");
for (const error of errors) {
logger.error(` - ${error}`);
}
throw new Error("CLI bootstrap static graph imports external packages.");
}
if (isDirectRunUrl(process.argv[1], import.meta.url)) {
try {
checkCliBootstrapExternalImports({ requireNativeHookRelay: true });
console.log("CLI bootstrap import guard passed.");
} catch {
process.exit(1);
}
}