#!/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 * as ts from "typescript/unstable/ast"; import { resolveNodeRuntimeExecutable } from "../src/infra/node-runtime-executable.ts"; import { collectSourceCheckoutPluginBuildEntries } from "./lib/bundled-plugin-build-entries.mjs"; import { createNativeTypeScriptParser, type NativeTypeScriptParser, } from "./lib/native-typescript.mts"; import { isRecord } from "./lib/record-shared.mjs"; import { resolveRepoRoot } from "./lib/repo-root.mjs"; 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([ ["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) { return ts.isIdentifier(name) || ts.isStringLiteralLikeNode(name) ? name.text : ""; } function listLegacySetupModuleSpecifiers(setupEntryPath: string, parser: NativeTypeScriptParser) { const source = fs.readFileSync(setupEntryPath, "utf8"); const sourceFile = parser.parseSourceFile(setupEntryPath, source); 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.isStringLiteralLikeNode(specifierProperty.initializer) ) { specifiers.push({ kind, specifier: specifierProperty.initializer.text }); } } } node.forEachChild(visit); }; visit(sourceFile); return specifiers; } /** Lists exact built doctor, contract, and channel legacy migration artifacts. */ export function listBuiltPluginControlPlaneModules( params: Pick = {}, ) { const rootDir = path.resolve(params.rootDir ?? ROOT); using parser = createNativeTypeScriptParser({ cwd: rootDir }); 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(); 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, parser)) { 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(sourceFile: ts.SourceFile): string[] { const specifiers: string[] = []; const visit = (node: ts.Node): void => { if (ts.isFunctionLikeDeclaration(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.isStringLiteralLikeNode(moduleSpecifier)) { specifiers.push(moduleSpecifier.text); } node.forEachChild(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, parser: NativeTypeScriptParser, moduleSpecifiers: Map, ): Map { const dependencies = new Map(); const visited = new Set(); const pending: string[] = [entryPath]; while (pending.length > 0) { const filePath = pending.pop(); if (!filePath || visited.has(filePath)) { continue; } visited.add(filePath); let specifiers = moduleSpecifiers.get(filePath); if (!specifiers) { let source: string; try { source = fs.readFileSync(filePath, "utf8"); } catch { continue; } specifiers = parseStaticModuleSpecifiers(parser.parseSourceFile(filePath, source)); moduleSpecifiers.set(filePath, specifiers); } for (const reference of specifiers) { 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); using parser = createNativeTypeScriptParser({ cwd: rootDir }); const violations: BuiltDoctorContractClosureViolation[] = []; // Shared chunks are parsed once per check; each contract retains its own traversal and attribution. const moduleSpecifiers = new Map(); for (const module of modules.filter((candidate) => candidate.kind === "doctor-contract")) { const dependencies = collectBuiltModuleStaticDependencies( path.join(rootDir, module.relativePath), parser, moduleSpecifiers, ); 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(); }