mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-23 07:51:33 +00:00
* Plugins: stabilize Area 6 loader and Docker smoke * Docker: fail fast on extension npm install errors * Tests: stabilize loader non-native Jiti boundary CI timeout * Tests: stabilize plugin loader Jiti source-runtime coverage * Docker: keep extension deps on lockfile graph * Tests: cover tsx-cache renamed package cwd fallback * Tests: stabilize plugin-sdk export subpath assertions * Plugins: align tsx-cache alias fallback with subpath fallback * Tests: normalize guardrail path checks for Windows * Plugins: restrict plugin-sdk cwd fallback to trusted roots * Tests: exempt outbound-session from extension import guard * Tests: tighten guardrails and cli-entry trust coverage * Tests: guard optional loader fixture exports * Tests: make loader fixture package exports null-safe * Tests: make loader fixture package exports null-safe * Tests: make loader fixture package exports null-safe * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
267 lines
8.2 KiB
TypeScript
267 lines
8.2 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
|
|
|
type PluginSdkAliasCandidateKind = "dist" | "src";
|
|
|
|
export type LoaderModuleResolveParams = {
|
|
modulePath?: string;
|
|
argv1?: string;
|
|
cwd?: string;
|
|
moduleUrl?: string;
|
|
};
|
|
|
|
type PluginSdkPackageJson = {
|
|
exports?: Record<string, unknown>;
|
|
bin?: string | Record<string, unknown>;
|
|
};
|
|
|
|
function resolveLoaderModulePath(params: LoaderModuleResolveParams = {}): string {
|
|
return params.modulePath ?? fileURLToPath(params.moduleUrl ?? import.meta.url);
|
|
}
|
|
|
|
function readPluginSdkPackageJson(packageRoot: string): PluginSdkPackageJson | null {
|
|
try {
|
|
const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8");
|
|
return JSON.parse(pkgRaw) as PluginSdkPackageJson;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function listPluginSdkSubpathsFromPackageJson(pkg: PluginSdkPackageJson): string[] {
|
|
return Object.keys(pkg.exports ?? {})
|
|
.filter((key) => key.startsWith("./plugin-sdk/"))
|
|
.map((key) => key.slice("./plugin-sdk/".length))
|
|
.filter((subpath) => Boolean(subpath) && !subpath.includes("/"))
|
|
.toSorted();
|
|
}
|
|
|
|
function hasTrustedOpenClawRootIndicator(params: {
|
|
packageRoot: string;
|
|
packageJson: PluginSdkPackageJson;
|
|
}): boolean {
|
|
const packageExports = params.packageJson.exports ?? {};
|
|
const hasPluginSdkRootExport = Object.prototype.hasOwnProperty.call(
|
|
packageExports,
|
|
"./plugin-sdk",
|
|
);
|
|
if (!hasPluginSdkRootExport) {
|
|
return false;
|
|
}
|
|
const hasCliEntryExport = Object.prototype.hasOwnProperty.call(packageExports, "./cli-entry");
|
|
const hasOpenClawBin =
|
|
(typeof params.packageJson.bin === "string" &&
|
|
params.packageJson.bin.toLowerCase().includes("openclaw")) ||
|
|
(typeof params.packageJson.bin === "object" &&
|
|
params.packageJson.bin !== null &&
|
|
typeof params.packageJson.bin.openclaw === "string");
|
|
const hasOpenClawEntrypoint = fs.existsSync(path.join(params.packageRoot, "openclaw.mjs"));
|
|
return hasCliEntryExport || hasOpenClawBin || hasOpenClawEntrypoint;
|
|
}
|
|
|
|
function readPluginSdkSubpathsFromPackageRoot(packageRoot: string): string[] | null {
|
|
const pkg = readPluginSdkPackageJson(packageRoot);
|
|
if (!pkg) {
|
|
return null;
|
|
}
|
|
if (!hasTrustedOpenClawRootIndicator({ packageRoot, packageJson: pkg })) {
|
|
return null;
|
|
}
|
|
const subpaths = listPluginSdkSubpathsFromPackageJson(pkg);
|
|
return subpaths.length > 0 ? subpaths : null;
|
|
}
|
|
|
|
function findNearestPluginSdkPackageRoot(startDir: string, maxDepth = 12): string | null {
|
|
let cursor = path.resolve(startDir);
|
|
for (let i = 0; i < maxDepth; i += 1) {
|
|
const subpaths = readPluginSdkSubpathsFromPackageRoot(cursor);
|
|
if (subpaths) {
|
|
return cursor;
|
|
}
|
|
const parent = path.dirname(cursor);
|
|
if (parent === cursor) {
|
|
break;
|
|
}
|
|
cursor = parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveLoaderPackageRoot(
|
|
params: LoaderModuleResolveParams & { modulePath: string },
|
|
): string | null {
|
|
const cwd = params.cwd ?? path.dirname(params.modulePath);
|
|
const fromModulePath = resolveOpenClawPackageRootSync({ cwd });
|
|
if (fromModulePath) {
|
|
return fromModulePath;
|
|
}
|
|
const argv1 = params.argv1 ?? process.argv[1];
|
|
const moduleUrl = params.moduleUrl ?? (params.modulePath ? undefined : import.meta.url);
|
|
return resolveOpenClawPackageRootSync({
|
|
cwd,
|
|
...(argv1 ? { argv1 } : {}),
|
|
...(moduleUrl ? { moduleUrl } : {}),
|
|
});
|
|
}
|
|
|
|
function resolveLoaderPluginSdkPackageRoot(
|
|
params: LoaderModuleResolveParams & { modulePath: string },
|
|
): string | null {
|
|
const cwd = params.cwd ?? path.dirname(params.modulePath);
|
|
const fromCwd = resolveOpenClawPackageRootSync({ cwd });
|
|
const fromExplicitHints =
|
|
params.argv1 || params.moduleUrl
|
|
? resolveOpenClawPackageRootSync({
|
|
cwd,
|
|
...(params.argv1 ? { argv1: params.argv1 } : {}),
|
|
...(params.moduleUrl ? { moduleUrl: params.moduleUrl } : {}),
|
|
})
|
|
: null;
|
|
return (
|
|
fromCwd ??
|
|
fromExplicitHints ??
|
|
findNearestPluginSdkPackageRoot(path.dirname(params.modulePath)) ??
|
|
(params.cwd ? findNearestPluginSdkPackageRoot(params.cwd) : null) ??
|
|
findNearestPluginSdkPackageRoot(process.cwd())
|
|
);
|
|
}
|
|
|
|
export function resolvePluginSdkAliasCandidateOrder(params: {
|
|
modulePath: string;
|
|
isProduction: boolean;
|
|
}): PluginSdkAliasCandidateKind[] {
|
|
const normalizedModulePath = params.modulePath.replace(/\\/g, "/");
|
|
const isDistRuntime = normalizedModulePath.includes("/dist/");
|
|
return isDistRuntime || params.isProduction ? ["dist", "src"] : ["src", "dist"];
|
|
}
|
|
|
|
export function listPluginSdkAliasCandidates(params: {
|
|
srcFile: string;
|
|
distFile: string;
|
|
modulePath: string;
|
|
argv1?: string;
|
|
cwd?: string;
|
|
moduleUrl?: string;
|
|
}) {
|
|
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
|
|
modulePath: params.modulePath,
|
|
isProduction: process.env.NODE_ENV === "production",
|
|
});
|
|
const packageRoot = resolveLoaderPluginSdkPackageRoot(params);
|
|
if (packageRoot) {
|
|
const candidateMap = {
|
|
src: path.join(packageRoot, "src", "plugin-sdk", params.srcFile),
|
|
dist: path.join(packageRoot, "dist", "plugin-sdk", params.distFile),
|
|
} as const;
|
|
return orderedKinds.map((kind) => candidateMap[kind]);
|
|
}
|
|
let cursor = path.dirname(params.modulePath);
|
|
const candidates: string[] = [];
|
|
for (let i = 0; i < 6; i += 1) {
|
|
const candidateMap = {
|
|
src: path.join(cursor, "src", "plugin-sdk", params.srcFile),
|
|
dist: path.join(cursor, "dist", "plugin-sdk", params.distFile),
|
|
} as const;
|
|
for (const kind of orderedKinds) {
|
|
candidates.push(candidateMap[kind]);
|
|
}
|
|
const parent = path.dirname(cursor);
|
|
if (parent === cursor) {
|
|
break;
|
|
}
|
|
cursor = parent;
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
export function resolvePluginSdkAliasFile(params: {
|
|
srcFile: string;
|
|
distFile: string;
|
|
modulePath?: string;
|
|
argv1?: string;
|
|
cwd?: string;
|
|
moduleUrl?: string;
|
|
}): string | null {
|
|
try {
|
|
const modulePath = resolveLoaderModulePath(params);
|
|
for (const candidate of listPluginSdkAliasCandidates({
|
|
srcFile: params.srcFile,
|
|
distFile: params.distFile,
|
|
modulePath,
|
|
argv1: params.argv1,
|
|
cwd: params.cwd,
|
|
moduleUrl: params.moduleUrl,
|
|
})) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
|
|
|
|
export function listPluginSdkExportedSubpaths(params: { modulePath?: string } = {}): string[] {
|
|
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
|
|
const packageRoot = resolveLoaderPluginSdkPackageRoot({ modulePath });
|
|
if (!packageRoot) {
|
|
return [];
|
|
}
|
|
const cached = cachedPluginSdkExportedSubpaths.get(packageRoot);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const subpaths = readPluginSdkSubpathsFromPackageRoot(packageRoot) ?? [];
|
|
cachedPluginSdkExportedSubpaths.set(packageRoot, subpaths);
|
|
return subpaths;
|
|
}
|
|
|
|
export function resolvePluginSdkScopedAliasMap(
|
|
params: { modulePath?: string } = {},
|
|
): Record<string, string> {
|
|
const aliasMap: Record<string, string> = {};
|
|
for (const subpath of listPluginSdkExportedSubpaths(params)) {
|
|
const resolved = resolvePluginSdkAliasFile({
|
|
srcFile: `${subpath}.ts`,
|
|
distFile: `${subpath}.js`,
|
|
modulePath: params.modulePath,
|
|
});
|
|
if (resolved) {
|
|
aliasMap[`openclaw/plugin-sdk/${subpath}`] = resolved;
|
|
}
|
|
}
|
|
return aliasMap;
|
|
}
|
|
|
|
export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
|
|
return {
|
|
interopDefault: true,
|
|
// Prefer Node's native sync ESM loader for built dist/*.js modules so
|
|
// bundled plugins and plugin-sdk subpaths stay on the canonical module graph.
|
|
tryNative: true,
|
|
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
|
|
...(Object.keys(aliasMap).length > 0
|
|
? {
|
|
alias: aliasMap,
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function shouldPreferNativeJiti(modulePath: string): boolean {
|
|
switch (path.extname(modulePath).toLowerCase()) {
|
|
case ".js":
|
|
case ".mjs":
|
|
case ".cjs":
|
|
case ".json":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|