mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
Merged via squash.
Prepared head SHA: 769e90c6af
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import path, { dirname, relative, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import ts from "typescript";
|
|
import { describe, expect, it } from "vitest";
|
|
import { listBundledPluginMetadata } from "../bundled-plugin-metadata.js";
|
|
import { loadPluginManifestRegistry } from "../manifest-registry.js";
|
|
|
|
const REPO_ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
|
const RUNTIME_ENTRY_HELPER_RE = /(^|\/)plugin-entry\.runtime\.[cm]?[jt]s$/;
|
|
const SOURCE_MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"] as const;
|
|
const FORBIDDEN_CONTRACT_MODULE_SPECIFIER_PATTERNS = [
|
|
/^vitest$/u,
|
|
/^openclaw\/plugin-sdk\/testing$/u,
|
|
/(^|\/)test-api(?:\.[cm]?[jt]s)?$/u,
|
|
/(^|\/)__tests__(\/|$)/u,
|
|
/(^|\/)test-support(\/|$)/u,
|
|
/(^|\/)[^/]*\.test(?:[-.][^/]*)?(?:\.[cm]?[jt]s)?$/u,
|
|
/(^|\/)[^/]*(?:test-harness|test-plugin|test-helper|test-support|harness)[^/]*(?:\.[cm]?[jt]s)?$/u,
|
|
] as const;
|
|
const FORBIDDEN_CONTRACT_MODULE_PATH_PATTERNS = [
|
|
/(^|\/)__tests__(\/|$)/u,
|
|
/(^|\/)test-support(\/|$)/u,
|
|
/(^|\/)test-api\.[cm]?[jt]s$/u,
|
|
/(^|\/)[^/]*\.test(?:[-.][^/]*)?\.[cm]?[jt]s$/u,
|
|
/(^|\/)[^/]*(?:test-harness|test-plugin|test-helper|test-support|harness)[^/]*\.[cm]?[jt]s$/u,
|
|
] as const;
|
|
function listBundledPluginRoots() {
|
|
return loadPluginManifestRegistry({})
|
|
.plugins.filter((plugin) => plugin.origin === "bundled")
|
|
.map((plugin) => ({
|
|
pluginId: plugin.id,
|
|
rootDir: plugin.workspaceDir ?? plugin.rootDir,
|
|
}))
|
|
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
}
|
|
|
|
function resolvePublicSurfaceSourcePath(
|
|
pluginDir: string,
|
|
artifactBasename: string,
|
|
): string | null {
|
|
const stem = artifactBasename.replace(/\.[^.]+$/u, "");
|
|
for (const extension of SOURCE_MODULE_EXTENSIONS) {
|
|
const candidate = resolve(pluginDir, `${stem}${extension}`);
|
|
if (existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isGuardedContractArtifactBasename(artifactBasename: string): boolean {
|
|
return (
|
|
artifactBasename === "channel-config-api.js" || artifactBasename.endsWith("contract-api.js")
|
|
);
|
|
}
|
|
|
|
function collectProductionContractEntryPaths(): Array<{
|
|
pluginId: string;
|
|
entryPath: string;
|
|
pluginRoot: string;
|
|
}> {
|
|
return listBundledPluginMetadata({ rootDir: REPO_ROOT }).flatMap((plugin) => {
|
|
const pluginRoot = resolve(REPO_ROOT, "extensions", plugin.dirName);
|
|
const entryPaths = new Set<string>();
|
|
for (const artifact of plugin.publicSurfaceArtifacts ?? []) {
|
|
if (!isGuardedContractArtifactBasename(artifact)) {
|
|
continue;
|
|
}
|
|
const sourcePath = resolvePublicSurfaceSourcePath(pluginRoot, artifact);
|
|
if (sourcePath) {
|
|
entryPaths.add(sourcePath);
|
|
}
|
|
}
|
|
return [...entryPaths].map((entryPath) => ({
|
|
pluginId: plugin.manifest.id,
|
|
entryPath,
|
|
pluginRoot,
|
|
}));
|
|
});
|
|
}
|
|
|
|
function formatRepoRelativePath(filePath: string): string {
|
|
return relative(REPO_ROOT, filePath).replaceAll(path.sep, "/");
|
|
}
|
|
|
|
function analyzeSourceModule(params: { filePath: string; source: string }): {
|
|
specifiers: string[];
|
|
relativeSpecifiers: string[];
|
|
importsDefinePluginEntryFromCore: boolean;
|
|
} {
|
|
const sourceFile = ts.createSourceFile(
|
|
params.filePath,
|
|
params.source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
);
|
|
const specifiers = new Set<string>();
|
|
let importsDefinePluginEntryFromCore = false;
|
|
|
|
for (const statement of sourceFile.statements) {
|
|
if (ts.isImportDeclaration(statement)) {
|
|
const specifier = ts.isStringLiteral(statement.moduleSpecifier)
|
|
? statement.moduleSpecifier.text
|
|
: undefined;
|
|
if (specifier) {
|
|
specifiers.add(specifier);
|
|
}
|
|
|
|
if (
|
|
specifier === "openclaw/plugin-sdk/core" &&
|
|
statement.importClause?.namedBindings &&
|
|
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
statement.importClause.namedBindings.elements.some(
|
|
(element) => (element.propertyName?.text ?? element.name.text) === "definePluginEntry",
|
|
)
|
|
) {
|
|
importsDefinePluginEntryFromCore = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!ts.isExportDeclaration(statement)) {
|
|
continue;
|
|
}
|
|
|
|
if (statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
specifiers.add(statement.moduleSpecifier.text);
|
|
}
|
|
}
|
|
|
|
const nextSpecifiers = [...specifiers];
|
|
return {
|
|
specifiers: nextSpecifiers,
|
|
relativeSpecifiers: nextSpecifiers.filter((specifier) => specifier.startsWith(".")),
|
|
importsDefinePluginEntryFromCore,
|
|
};
|
|
}
|
|
|
|
function matchesForbiddenContractSpecifier(specifier: string): boolean {
|
|
return FORBIDDEN_CONTRACT_MODULE_SPECIFIER_PATTERNS.some((pattern) => pattern.test(specifier));
|
|
}
|
|
|
|
function collectForbiddenContractSpecifiers(specifiers: readonly string[]): string[] {
|
|
return specifiers.filter((specifier) => matchesForbiddenContractSpecifier(specifier));
|
|
}
|
|
|
|
function resolveRelativeSourceModulePath(fromPath: string, specifier: string): string | null {
|
|
const rawTargetPath = resolve(dirname(fromPath), specifier);
|
|
const candidates = new Set<string>();
|
|
const rawExtension = path.extname(rawTargetPath);
|
|
if (rawExtension) {
|
|
candidates.add(rawTargetPath);
|
|
const stem = rawTargetPath.slice(0, -rawExtension.length);
|
|
for (const extension of SOURCE_MODULE_EXTENSIONS) {
|
|
candidates.add(`${stem}${extension}`);
|
|
}
|
|
} else {
|
|
for (const extension of SOURCE_MODULE_EXTENSIONS) {
|
|
candidates.add(`${rawTargetPath}${extension}`);
|
|
candidates.add(resolve(rawTargetPath, `index${extension}`));
|
|
}
|
|
}
|
|
|
|
for (const candidate of candidates) {
|
|
if (existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findForbiddenContractModuleGraphPaths(params: {
|
|
entryPath: string;
|
|
pluginRoot: string;
|
|
}): string[] {
|
|
const failures: string[] = [];
|
|
const visited = new Set<string>();
|
|
const pending = [params.entryPath];
|
|
|
|
while (pending.length > 0) {
|
|
const currentPath = pending.pop();
|
|
if (!currentPath || visited.has(currentPath)) {
|
|
continue;
|
|
}
|
|
visited.add(currentPath);
|
|
|
|
const repoRelativePath = formatRepoRelativePath(currentPath);
|
|
for (const pattern of FORBIDDEN_CONTRACT_MODULE_PATH_PATTERNS) {
|
|
if (pattern.test(repoRelativePath)) {
|
|
failures.push(`${repoRelativePath} matched ${pattern}`);
|
|
}
|
|
}
|
|
|
|
const source = readFileSync(currentPath, "utf8");
|
|
const analysis = analyzeSourceModule({ filePath: currentPath, source });
|
|
for (const specifier of collectForbiddenContractSpecifiers(analysis.specifiers)) {
|
|
failures.push(`${repoRelativePath} imported ${specifier}`);
|
|
}
|
|
|
|
for (const specifier of analysis.relativeSpecifiers) {
|
|
const resolvedModulePath = resolveRelativeSourceModulePath(currentPath, specifier);
|
|
if (!resolvedModulePath) {
|
|
continue;
|
|
}
|
|
if (resolvedModulePath === currentPath) {
|
|
continue;
|
|
}
|
|
if (!resolvedModulePath.startsWith(params.pluginRoot + path.sep)) {
|
|
continue;
|
|
}
|
|
pending.push(resolvedModulePath);
|
|
}
|
|
}
|
|
|
|
return failures;
|
|
}
|
|
|
|
describe("plugin entry guardrails", () => {
|
|
it("keeps bundled extension entry modules off direct definePluginEntry imports from core", () => {
|
|
const failures: string[] = [];
|
|
|
|
for (const plugin of listBundledPluginRoots()) {
|
|
const indexPath = resolve(plugin.rootDir, "index.ts");
|
|
try {
|
|
const source = readFileSync(indexPath, "utf8");
|
|
if (analyzeSourceModule({ filePath: indexPath, source }).importsDefinePluginEntryFromCore) {
|
|
failures.push(`extensions/${plugin.pluginId}/index.ts`);
|
|
}
|
|
} catch {
|
|
// Skip extensions without index.ts entry modules.
|
|
}
|
|
}
|
|
|
|
expect(failures).toEqual([]);
|
|
});
|
|
|
|
it("does not advertise runtime helper sidecars as bundled plugin entry extensions", () => {
|
|
const failures: string[] = [];
|
|
|
|
for (const plugin of listBundledPluginRoots()) {
|
|
const packageJsonPath = resolve(plugin.rootDir, "package.json");
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
openclaw?: { extensions?: unknown };
|
|
};
|
|
const extensions = Array.isArray(pkg.openclaw?.extensions) ? pkg.openclaw.extensions : [];
|
|
if (
|
|
extensions.some(
|
|
(candidate) => typeof candidate === "string" && RUNTIME_ENTRY_HELPER_RE.test(candidate),
|
|
)
|
|
) {
|
|
failures.push(`extensions/${plugin.pluginId}/package.json`);
|
|
}
|
|
} catch {
|
|
// Skip directories without package metadata.
|
|
}
|
|
}
|
|
|
|
expect(failures).toEqual([]);
|
|
});
|
|
|
|
it("keeps bundled production contract barrels off test-only imports and re-exports", () => {
|
|
const failures = collectProductionContractEntryPaths().flatMap(
|
|
({ pluginId, entryPath, pluginRoot }) =>
|
|
findForbiddenContractModuleGraphPaths({
|
|
entryPath,
|
|
pluginRoot,
|
|
}).map((failure) => `${pluginId}: ${failure}`),
|
|
);
|
|
|
|
expect(failures).toEqual([]);
|
|
});
|
|
|
|
it("follows relative import edges while scanning guarded contract graphs", () => {
|
|
expect(
|
|
analyzeSourceModule({
|
|
filePath: "guardrail-fixture.ts",
|
|
source: `
|
|
import { x } from "./safe.js";
|
|
import "./setup.js";
|
|
export { x };
|
|
export * from "./barrel.js";
|
|
import { y } from "openclaw/plugin-sdk/testing";
|
|
`,
|
|
}).relativeSpecifiers.toSorted(),
|
|
).toEqual(["./barrel.js", "./safe.js", "./setup.js"]);
|
|
});
|
|
|
|
it("guards contract-style production artifacts beyond the legacy allowlist", () => {
|
|
expect(isGuardedContractArtifactBasename("channel-config-api.js")).toBe(true);
|
|
expect(isGuardedContractArtifactBasename("contract-api.js")).toBe(true);
|
|
expect(isGuardedContractArtifactBasename("doctor-contract-api.js")).toBe(true);
|
|
expect(isGuardedContractArtifactBasename("web-search-contract-api.js")).toBe(true);
|
|
expect(isGuardedContractArtifactBasename("test-api.js")).toBe(false);
|
|
});
|
|
|
|
it("flags test-support directory hops in guarded contract graphs", () => {
|
|
expect(collectForbiddenContractSpecifiers(["./test-support/index.js"])).toEqual([
|
|
"./test-support/index.js",
|
|
]);
|
|
expect(
|
|
FORBIDDEN_CONTRACT_MODULE_PATH_PATTERNS.some((pattern) =>
|
|
pattern.test("extensions/demo/src/test-support/index.ts"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("detects aliased definePluginEntry imports from core", () => {
|
|
expect(
|
|
analyzeSourceModule({
|
|
filePath: "aliased-plugin-entry.ts",
|
|
source: `
|
|
import { definePluginEntry as dpe } from "openclaw/plugin-sdk/core";
|
|
import { somethingElse } from "openclaw/plugin-sdk/core";
|
|
`,
|
|
}).importsDefinePluginEntryFromCore,
|
|
).toBe(true);
|
|
});
|
|
});
|