mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* fix(runtime): support Bun across tooling suite * fix(security): guard Anthropic probe upstream requests * fix(runtime): preserve native TypeScript script loading
328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
#!/usr/bin/env node
|
|
|
|
// Verifies built plugin control-plane artifacts through Node's native require(esm) path.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import type ts from "typescript";
|
|
import { resolveNodeRuntimeExecutable } from "../src/infra/node-runtime-executable.ts";
|
|
import { collectSourceCheckoutPluginBuildEntries } from "./lib/bundled-plugin-build-entries.mjs";
|
|
import { isRecord } from "./lib/record-shared.mjs";
|
|
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
|
import { getTypeScript } from "./lib/ts-guard-utils.mts";
|
|
|
|
type BuiltPluginControlPlaneModule = {
|
|
pluginId: string;
|
|
kind: string;
|
|
relativePath: string;
|
|
};
|
|
|
|
type BuiltPluginControlPlaneModuleFailure = BuiltPluginControlPlaneModule & {
|
|
error: string;
|
|
};
|
|
|
|
type BuiltDoctorContractClosureViolation = BuiltPluginControlPlaneModule & {
|
|
dependency: string;
|
|
importerPath: string;
|
|
};
|
|
|
|
type ProbeParams = {
|
|
rootDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
const ROOT = resolveRepoRoot(import.meta.url);
|
|
const DIRECT_CONTRACT_ENTRIES = ["contract-api", "doctor-contract-api"];
|
|
const LEGACY_SETUP_PROPERTIES = new Map<string, string>([
|
|
["legacyStateMigrations", "channel-legacy-state-migrations"],
|
|
["legacySessionSurface", "channel-legacy-session-surface"],
|
|
["legacySessionSurfaces", "channel-legacy-session-surface"],
|
|
]);
|
|
const PROBE_RESULT_MARKER = "__OPENCLAW_PLUGIN_CONTROL_PLANE_PROBE__";
|
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
// Doctor enumeration cold-loads every declaring plugin's contract closure, so a
|
|
// doctor artifact must never reach the process-spawn graph. Requiring the artifact
|
|
// cannot prove this: plain Node resolves the whole graph fine, and the cost and the
|
|
// ESM-only transitive deps (execa -> npm-run-path -> unicorn-magic, which has no
|
|
// `require` condition) only surface on source-run hosts whose CJS-flavored resolver
|
|
// rejects them. `doctor-contract-closure-guard.test.ts` owns the same invariant over
|
|
// sources; bundling can merge runtime code into the artifact behind its back, so the
|
|
// built closure is checked here.
|
|
const FORBIDDEN_DOCTOR_CONTRACT_DEPENDENCIES = ["execa"];
|
|
const REQUIRE_PROBE_SOURCE = String.raw`
|
|
const { createRequire } = require("node:module");
|
|
const path = require("node:path");
|
|
const targets = JSON.parse(Buffer.from(process.argv[1], "base64url").toString("utf8"));
|
|
const requireFromRoot = createRequire(path.join(process.cwd(), "package.json"));
|
|
const failures = [];
|
|
for (const target of targets) {
|
|
try {
|
|
requireFromRoot(path.resolve(process.cwd(), target.relativePath));
|
|
} catch (error) {
|
|
failures.push({
|
|
...target,
|
|
error: error instanceof Error ? (error.stack || error.message) : String(error),
|
|
});
|
|
}
|
|
}
|
|
process.stdout.write("\n${PROBE_RESULT_MARKER}" + JSON.stringify({ failures }));
|
|
`;
|
|
|
|
function propertyNameText(name: ts.PropertyName) {
|
|
const ts = getTypeScript();
|
|
return ts.isIdentifier(name) || ts.isStringLiteralLike(name) ? name.text : "";
|
|
}
|
|
|
|
function listLegacySetupModuleSpecifiers(setupEntryPath: string) {
|
|
const ts = getTypeScript();
|
|
const source = fs.readFileSync(setupEntryPath, "utf8");
|
|
const sourceFile = ts.createSourceFile(setupEntryPath, source, ts.ScriptTarget.Latest, true);
|
|
const specifiers: Array<{ kind: string; specifier: string }> = [];
|
|
const visit = (node: ts.Node): void => {
|
|
if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.initializer)) {
|
|
const kind = LEGACY_SETUP_PROPERTIES.get(propertyNameText(node.name));
|
|
if (kind) {
|
|
const specifierProperty = node.initializer.properties.find(
|
|
(property) =>
|
|
ts.isPropertyAssignment(property) && propertyNameText(property.name) === "specifier",
|
|
);
|
|
if (
|
|
specifierProperty &&
|
|
ts.isPropertyAssignment(specifierProperty) &&
|
|
ts.isStringLiteralLike(specifierProperty.initializer)
|
|
) {
|
|
specifiers.push({ kind, specifier: specifierProperty.initializer.text });
|
|
}
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return specifiers;
|
|
}
|
|
|
|
/** Lists exact built doctor, contract, and channel legacy migration artifacts. */
|
|
export function listBuiltPluginControlPlaneModules(
|
|
params: Pick<ProbeParams, "rootDir" | "env"> = {},
|
|
) {
|
|
const rootDir = path.resolve(params.rootDir ?? ROOT);
|
|
const extensionsDir = path.join(rootDir, "dist", "extensions");
|
|
if (!fs.existsSync(extensionsDir)) {
|
|
return [];
|
|
}
|
|
const sourceEntries = new Map(
|
|
(fs.existsSync(path.join(rootDir, "extensions"))
|
|
? collectSourceCheckoutPluginBuildEntries({ cwd: rootDir, env: params.env })
|
|
: []
|
|
).map((entry) => [entry.id, entry]),
|
|
);
|
|
const modules = new Map<string, BuiltPluginControlPlaneModule>();
|
|
for (const entry of fs
|
|
.readdirSync(extensionsDir, { withFileTypes: true })
|
|
.filter((candidate) => candidate.isDirectory())
|
|
.toSorted((left, right) => left.name.localeCompare(right.name))) {
|
|
const pluginId = entry.name;
|
|
const pluginDir = path.join(extensionsDir, pluginId);
|
|
// Packaged core artifacts use ESM; isolated source-checkout plugins use
|
|
// the same selected format as their builder and generated metadata.
|
|
const extension = sourceEntries.get(pluginId)?.runtimeExtension ?? ".js";
|
|
for (const entryName of DIRECT_CONTRACT_ENTRIES) {
|
|
const fileName = `${entryName}${extension}`;
|
|
const modulePath = path.join(pluginDir, fileName);
|
|
if (fs.existsSync(modulePath)) {
|
|
const relativePath = path.relative(rootDir, modulePath).split(path.sep).join("/");
|
|
modules.set(relativePath, {
|
|
pluginId,
|
|
kind: entryName === "doctor-contract-api" ? "doctor-contract" : "contract",
|
|
relativePath,
|
|
});
|
|
}
|
|
}
|
|
const setupEntryPath = path.join(pluginDir, `setup-entry${extension}`);
|
|
if (!fs.existsSync(setupEntryPath)) {
|
|
continue;
|
|
}
|
|
for (const { kind, specifier } of listLegacySetupModuleSpecifiers(setupEntryPath)) {
|
|
const modulePath = path.resolve(pluginDir, specifier);
|
|
const pluginRelativePath = path.relative(pluginDir, modulePath);
|
|
if (pluginRelativePath.startsWith(`..${path.sep}`) || path.isAbsolute(pluginRelativePath)) {
|
|
throw new Error(`${pluginId} setup entry module escapes the plugin root: ${specifier}`);
|
|
}
|
|
const relativePath = path.relative(rootDir, modulePath).split(path.sep).join("/");
|
|
modules.set(relativePath, { pluginId, kind, relativePath });
|
|
}
|
|
}
|
|
return [...modules.values()].toSorted((left, right) =>
|
|
left.relativePath.localeCompare(right.relativePath),
|
|
);
|
|
}
|
|
|
|
/** Loads every selected artifact in one timeout-bounded native-require child. */
|
|
export function probeBuiltPluginControlPlaneModules(
|
|
modules: BuiltPluginControlPlaneModule[],
|
|
params: ProbeParams = {},
|
|
) {
|
|
if (modules.length === 0) {
|
|
return [];
|
|
}
|
|
const rootDir = path.resolve(params.rootDir ?? ROOT);
|
|
const encodedTargets = Buffer.from(JSON.stringify(modules), "utf8").toString("base64url");
|
|
const result = spawnSync(
|
|
resolveNodeRuntimeExecutable() ?? process.execPath,
|
|
["-e", REQUIRE_PROBE_SOURCE, encodedTargets],
|
|
{
|
|
cwd: rootDir,
|
|
encoding: "utf8",
|
|
maxBuffer: 8 * 1024 * 1024,
|
|
timeout: params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
},
|
|
);
|
|
if (result.error) {
|
|
throw new Error(
|
|
`built plugin control-plane native-require probe failed: ${result.error.message}`,
|
|
);
|
|
}
|
|
const markerIndex = result.stdout.lastIndexOf(PROBE_RESULT_MARKER);
|
|
if (markerIndex < 0) {
|
|
throw new Error(
|
|
`built plugin control-plane native-require probe exited ${String(result.status)} without a result`,
|
|
);
|
|
}
|
|
const payload: unknown = JSON.parse(
|
|
result.stdout.slice(markerIndex + PROBE_RESULT_MARKER.length),
|
|
);
|
|
if (!isRecord(payload) || !Array.isArray(payload.failures)) {
|
|
return [];
|
|
}
|
|
return payload.failures.filter(
|
|
(failure): failure is BuiltPluginControlPlaneModuleFailure =>
|
|
isRecord(failure) &&
|
|
typeof failure.pluginId === "string" &&
|
|
typeof failure.kind === "string" &&
|
|
typeof failure.relativePath === "string" &&
|
|
typeof failure.error === "string",
|
|
);
|
|
}
|
|
|
|
// Follow ESM declarations and eager CJS require calls emitted by isolated builds.
|
|
// Dynamic imports and requires inside functions are lazy, not enumeration costs.
|
|
function parseStaticModuleSpecifiers(source: string, filePath: string): string[] {
|
|
const ts = getTypeScript();
|
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
const specifiers: string[] = [];
|
|
const visit = (node: ts.Node): void => {
|
|
if (ts.isFunctionLike(node)) {
|
|
return;
|
|
}
|
|
const moduleSpecifier =
|
|
ts.isImportDeclaration(node) || ts.isExportDeclaration(node)
|
|
? node.moduleSpecifier
|
|
: ts.isCallExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
/^(?:require|_+require\d*)$/u.test(node.expression.text)
|
|
? node.arguments[0]
|
|
: undefined;
|
|
if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) {
|
|
specifiers.push(moduleSpecifier.text);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return specifiers;
|
|
}
|
|
|
|
function resolveBuiltChunkPath(importerPath: string, specifier: string): string | undefined {
|
|
const target = path.resolve(path.dirname(importerPath), specifier);
|
|
// Generated chunk edges carry their exact output suffix, including CJS.
|
|
return fs.existsSync(target) && fs.statSync(target).isFile() ? target : undefined;
|
|
}
|
|
|
|
/** Collects the bare dependencies a built artifact reaches through static imports. */
|
|
function collectBuiltModuleStaticDependencies(entryPath: string): Map<string, string> {
|
|
const dependencies = new Map<string, string>();
|
|
const visited = new Set<string>();
|
|
const pending: string[] = [entryPath];
|
|
while (pending.length > 0) {
|
|
const filePath = pending.pop();
|
|
if (!filePath || visited.has(filePath)) {
|
|
continue;
|
|
}
|
|
visited.add(filePath);
|
|
let source: string;
|
|
try {
|
|
source = fs.readFileSync(filePath, "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const reference of parseStaticModuleSpecifiers(source, filePath)) {
|
|
if (reference.startsWith(".") || reference.startsWith("/")) {
|
|
const resolved = resolveBuiltChunkPath(filePath, reference);
|
|
if (resolved) {
|
|
pending.push(resolved);
|
|
}
|
|
continue;
|
|
}
|
|
if (!reference.startsWith("node:") && !dependencies.has(reference)) {
|
|
dependencies.set(reference, filePath);
|
|
}
|
|
}
|
|
}
|
|
return dependencies;
|
|
}
|
|
|
|
/** Fails when a built doctor artifact statically reaches a forbidden runtime dependency. */
|
|
export function collectBuiltDoctorContractClosureViolations(
|
|
modules: BuiltPluginControlPlaneModule[],
|
|
params: { rootDir?: string } = {},
|
|
): BuiltDoctorContractClosureViolation[] {
|
|
const rootDir = path.resolve(params.rootDir ?? ROOT);
|
|
const violations: BuiltDoctorContractClosureViolation[] = [];
|
|
for (const module of modules.filter((candidate) => candidate.kind === "doctor-contract")) {
|
|
const dependencies = collectBuiltModuleStaticDependencies(
|
|
path.join(rootDir, module.relativePath),
|
|
);
|
|
for (const dependency of FORBIDDEN_DOCTOR_CONTRACT_DEPENDENCIES) {
|
|
const importer = dependencies.get(dependency);
|
|
if (importer) {
|
|
violations.push({
|
|
...module,
|
|
dependency,
|
|
importerPath: path.relative(rootDir, importer).split(path.sep).join("/"),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
/** Fails the build when a generated plugin control-plane module cannot be required natively. */
|
|
export function verifyBuiltPluginControlPlaneModules(params: ProbeParams = {}) {
|
|
const modules = listBuiltPluginControlPlaneModules(params);
|
|
const failures = probeBuiltPluginControlPlaneModules(modules, params);
|
|
if (failures.length > 0) {
|
|
const details = failures.map(
|
|
(failure) =>
|
|
`- ${failure.pluginId} (${failure.kind}) ${failure.relativePath}: ${failure.error}`,
|
|
);
|
|
throw new Error(`built plugin control-plane module load failures:\n${details.join("\n")}`);
|
|
}
|
|
const closureViolations = collectBuiltDoctorContractClosureViolations(modules, params);
|
|
if (closureViolations.length > 0) {
|
|
const details = closureViolations.map(
|
|
(violation) =>
|
|
`- ${violation.pluginId} ${violation.relativePath} statically reaches ${violation.dependency} through ${violation.importerPath}`,
|
|
);
|
|
throw new Error(
|
|
`built doctor contract closures reach forbidden runtime dependencies:\n${details.join("\n")}`,
|
|
);
|
|
}
|
|
console.error(
|
|
`[plugin-control-plane-loads] verified ${modules.length} built modules with native require and checked doctor closures`,
|
|
);
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
verifyBuiltPluginControlPlaneModules();
|
|
}
|