mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 20:46:57 +02:00
chore: harden plugin boundary report
This commit is contained in:
@@ -89,10 +89,15 @@ For external plugins, compatibility work follows this order:
|
||||
6. remove only after the announced migration window, usually in a major release
|
||||
|
||||
Maintainers can audit the current migration queue with
|
||||
`pnpm plugins:boundary-report`. The report groups deprecated compatibility
|
||||
records by removal date, counts local code/docs references, surfaces cross-owner
|
||||
reserved SDK imports, and summarizes the private memory-host SDK bridge so
|
||||
compatibility cleanup stays explicit instead of relying on ad hoc searches.
|
||||
`pnpm plugins:boundary-report`. Use `pnpm plugins:boundary-report:summary` for
|
||||
compact counts, `--owner <id>` for one plugin or compatibility owner, and
|
||||
`pnpm plugins:boundary-report:ci` when a CI gate should fail on due
|
||||
compatibility records, cross-owner reserved SDK imports, or unused reserved SDK
|
||||
subpaths without a dormant classification. The report groups deprecated
|
||||
compatibility records by removal date, counts local code/docs references,
|
||||
surfaces cross-owner reserved SDK imports, classifies dormant reserved SDK
|
||||
subpaths, and summarizes the private memory-host SDK bridge so compatibility
|
||||
cleanup stays explicit instead of relying on ad hoc searches.
|
||||
|
||||
If a manifest field is still accepted, plugin authors can keep using it until
|
||||
the docs and diagnostics say otherwise. New code should prefer the documented
|
||||
|
||||
@@ -10,7 +10,8 @@ The plugin SDK is exposed as a set of narrow subpaths under `openclaw/plugin-sdk
|
||||
This page catalogs the commonly used subpaths grouped by purpose. The generated
|
||||
full list of 200+ subpaths lives in `scripts/lib/plugin-sdk-entrypoints.json`;
|
||||
reserved bundled-plugin helper subpaths appear there but are implementation
|
||||
detail unless a doc page explicitly promotes them.
|
||||
detail unless a doc page explicitly promotes them. Maintainers can audit active
|
||||
and dormant reserved helper subpaths with `pnpm plugins:boundary-report:summary`.
|
||||
|
||||
For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview).
|
||||
|
||||
|
||||
@@ -1582,7 +1582,9 @@
|
||||
"plugin-sdk:sync-exports": "node scripts/sync-plugin-sdk-exports.mjs",
|
||||
"plugin-sdk:usage": "node --import tsx scripts/analyze-plugin-sdk-usage.ts",
|
||||
"plugins:boundary-report": "node --import tsx scripts/plugin-boundary-report.ts",
|
||||
"plugins:boundary-report:ci": "node --import tsx scripts/plugin-boundary-report.ts --summary --fail-on-cross-owner --fail-on-unclassified-unused-reserved --fail-on-eligible-compat",
|
||||
"plugins:boundary-report:json": "node --import tsx scripts/plugin-boundary-report.ts --json",
|
||||
"plugins:boundary-report:summary": "node --import tsx scripts/plugin-boundary-report.ts --summary",
|
||||
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
||||
"postinstall": "node scripts/postinstall-bundled-plugins.mjs",
|
||||
"preinstall": "node scripts/preinstall-package-manager-warning.mjs",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import {
|
||||
dormantReservedBundledPluginSdkEntrypoints,
|
||||
pluginSdkEntrypoints,
|
||||
publicPluginOwnedSdkEntrypoints,
|
||||
reservedBundledPluginSdkEntrypoints,
|
||||
@@ -24,6 +25,16 @@ const TEXT_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/u;
|
||||
const PLUGIN_SDK_SPECIFIER_PATTERN =
|
||||
/\b(?:from\s*["']|import\s*\(\s*["']|require\s*\(\s*["']|vi\.(?:mock|doMock)\s*\(\s*["'])(openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*))["']/g;
|
||||
|
||||
type CliOptions = {
|
||||
json: boolean;
|
||||
summary: boolean;
|
||||
owner?: string;
|
||||
failOnCrossOwner: boolean;
|
||||
failOnEligibleCompat: boolean;
|
||||
failOnUnclassifiedUnusedReserved: boolean;
|
||||
help: boolean;
|
||||
};
|
||||
|
||||
type CompatDebtRecord = {
|
||||
code: string;
|
||||
owner: string;
|
||||
@@ -57,11 +68,14 @@ type BoundaryReport = {
|
||||
pluginSdk: {
|
||||
entrypointCount: number;
|
||||
reservedCount: number;
|
||||
dormantReservedCount: number;
|
||||
supportedBundledFacadeCount: number;
|
||||
publicPluginOwnedCount: number;
|
||||
reservedImports: ReservedSdkImport[];
|
||||
crossOwnerReservedImports: ReservedSdkImport[];
|
||||
unusedReservedSubpaths: string[];
|
||||
dormantReservedSubpaths: string[];
|
||||
unclassifiedUnusedReservedSubpaths: string[];
|
||||
};
|
||||
memoryHostSdk: {
|
||||
privatePackage: boolean;
|
||||
@@ -71,6 +85,38 @@ type BoundaryReport = {
|
||||
};
|
||||
};
|
||||
|
||||
type BoundaryReportSummary = {
|
||||
generatedAt: string;
|
||||
owner?: string;
|
||||
compat: {
|
||||
deprecatedCount: number;
|
||||
eligibleForRemovalCount: number;
|
||||
deprecatedByOwner: Record<string, number>;
|
||||
eligibleForRemoval: Array<Pick<CompatDebtRecord, "code" | "owner" | "removeAfter">>;
|
||||
};
|
||||
pluginSdk: {
|
||||
entrypointCount: number;
|
||||
reservedCount: number;
|
||||
dormantReservedCount: number;
|
||||
supportedBundledFacadeCount: number;
|
||||
publicPluginOwnedCount: number;
|
||||
reservedImportCount: number;
|
||||
crossOwnerReservedImportCount: number;
|
||||
unusedReservedCount: number;
|
||||
dormantReservedCountInUnused: number;
|
||||
unclassifiedUnusedReservedCount: number;
|
||||
unclassifiedUnusedReservedSubpaths: string[];
|
||||
crossOwnerReservedImports: ReservedSdkImport[];
|
||||
};
|
||||
memoryHostSdk: {
|
||||
privatePackage: boolean;
|
||||
exportedSubpathCount: number;
|
||||
sourceBridgeFileCount: number;
|
||||
packageCoreReferenceFileCount: number;
|
||||
implementation: "private-core-bridge" | "package-owned" | "mixed";
|
||||
};
|
||||
};
|
||||
|
||||
function collectTextFiles(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
if (!existsSync(dir)) {
|
||||
@@ -106,6 +152,57 @@ function isDocsFile(file: string): boolean {
|
||||
return file.startsWith("docs/") || file === "README.md";
|
||||
}
|
||||
|
||||
function parseArgs(args: readonly string[]): CliOptions {
|
||||
const options: CliOptions = {
|
||||
json: false,
|
||||
summary: false,
|
||||
failOnCrossOwner: false,
|
||||
failOnEligibleCompat: false,
|
||||
failOnUnclassifiedUnusedReserved: false,
|
||||
help: false,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--json") {
|
||||
options.json = true;
|
||||
} else if (arg === "--summary") {
|
||||
options.summary = true;
|
||||
} else if (arg === "--owner") {
|
||||
const owner = args[index + 1];
|
||||
if (!owner || owner.startsWith("--")) {
|
||||
throw new Error("--owner requires a plugin or compatibility owner id");
|
||||
}
|
||||
options.owner = owner;
|
||||
index += 1;
|
||||
} else if (arg === "--fail-on-cross-owner") {
|
||||
options.failOnCrossOwner = true;
|
||||
} else if (arg === "--fail-on-eligible-compat") {
|
||||
options.failOnEligibleCompat = true;
|
||||
} else if (arg === "--fail-on-unclassified-unused-reserved") {
|
||||
options.failOnUnclassifiedUnusedReserved = true;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
options.help = true;
|
||||
} else {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function renderHelp(): string {
|
||||
return [
|
||||
"Usage: pnpm plugins:boundary-report [--summary] [--json] [--owner <id>] [fail flags]",
|
||||
"",
|
||||
"Options:",
|
||||
" --summary Print compact counts only.",
|
||||
" --json Emit JSON instead of text.",
|
||||
" --owner <id> Filter compat/imports/reserved shims by owner id.",
|
||||
" --fail-on-cross-owner Exit non-zero on cross-owner reserved SDK imports.",
|
||||
" --fail-on-eligible-compat Exit non-zero when deprecated compat is due for removal.",
|
||||
" --fail-on-unclassified-unused-reserved Exit non-zero on unused reserved SDK shims without a dormant classification.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function collectBundledPluginIds(): string[] {
|
||||
return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
@@ -258,11 +355,95 @@ function collectMemoryHostBoundary(files: readonly string[]): BoundaryReport["me
|
||||
};
|
||||
}
|
||||
|
||||
function buildReport(): BoundaryReport {
|
||||
function matchesOwner(owner: string | undefined, value: string | undefined): boolean {
|
||||
return owner === undefined || value === owner;
|
||||
}
|
||||
|
||||
function countByOwner(records: readonly CompatDebtRecord[]): Record<string, number> {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const record of records) {
|
||||
counts[record.owner] = (counts[record.owner] ?? 0) + 1;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(counts).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMemoryHostImplementation(
|
||||
memoryHostSdk: BoundaryReport["memoryHostSdk"],
|
||||
): BoundaryReportSummary["memoryHostSdk"]["implementation"] {
|
||||
if (memoryHostSdk.privatePackage && memoryHostSdk.packageCoreReferenceFiles.length > 0) {
|
||||
return "private-core-bridge";
|
||||
}
|
||||
if (!memoryHostSdk.privatePackage && memoryHostSdk.packageCoreReferenceFiles.length === 0) {
|
||||
return "package-owned";
|
||||
}
|
||||
return "mixed";
|
||||
}
|
||||
|
||||
function buildSummary(report: BoundaryReport, owner?: string): BoundaryReportSummary {
|
||||
const eligibleForRemoval = report.compat.records
|
||||
.filter((record) => record.eligibleForRemoval)
|
||||
.map((record) => ({
|
||||
code: record.code,
|
||||
owner: record.owner,
|
||||
removeAfter: record.removeAfter,
|
||||
}));
|
||||
return {
|
||||
generatedAt: report.generatedAt,
|
||||
owner,
|
||||
compat: {
|
||||
deprecatedCount: report.compat.deprecatedCount,
|
||||
eligibleForRemovalCount: report.compat.eligibleForRemovalCount,
|
||||
deprecatedByOwner: countByOwner(report.compat.records),
|
||||
eligibleForRemoval,
|
||||
},
|
||||
pluginSdk: {
|
||||
entrypointCount: report.pluginSdk.entrypointCount,
|
||||
reservedCount: report.pluginSdk.reservedCount,
|
||||
dormantReservedCount: report.pluginSdk.dormantReservedCount,
|
||||
supportedBundledFacadeCount: report.pluginSdk.supportedBundledFacadeCount,
|
||||
publicPluginOwnedCount: report.pluginSdk.publicPluginOwnedCount,
|
||||
reservedImportCount: report.pluginSdk.reservedImports.length,
|
||||
crossOwnerReservedImportCount: report.pluginSdk.crossOwnerReservedImports.length,
|
||||
unusedReservedCount: report.pluginSdk.unusedReservedSubpaths.length,
|
||||
dormantReservedCountInUnused: report.pluginSdk.dormantReservedSubpaths.length,
|
||||
unclassifiedUnusedReservedCount: report.pluginSdk.unclassifiedUnusedReservedSubpaths.length,
|
||||
unclassifiedUnusedReservedSubpaths: report.pluginSdk.unclassifiedUnusedReservedSubpaths,
|
||||
crossOwnerReservedImports: report.pluginSdk.crossOwnerReservedImports,
|
||||
},
|
||||
memoryHostSdk: {
|
||||
privatePackage: report.memoryHostSdk.privatePackage,
|
||||
exportedSubpathCount: report.memoryHostSdk.exportedSubpaths.length,
|
||||
sourceBridgeFileCount: report.memoryHostSdk.sourceBridgeFiles.length,
|
||||
packageCoreReferenceFileCount: report.memoryHostSdk.packageCoreReferenceFiles.length,
|
||||
implementation: resolveMemoryHostImplementation(report.memoryHostSdk),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildReport(options: Pick<CliOptions, "owner"> = {}): BoundaryReport {
|
||||
const files = collectWorkspaceTextFiles();
|
||||
const compatRecords = collectCompatDebt(files);
|
||||
const reservedImports = collectReservedSdkImports(files);
|
||||
const pluginIds = collectBundledPluginIds();
|
||||
const compatRecords = collectCompatDebt(files).filter((record) =>
|
||||
matchesOwner(options.owner, record.owner),
|
||||
);
|
||||
const reservedImports = collectReservedSdkImports(files).filter(
|
||||
(entry) =>
|
||||
matchesOwner(options.owner, entry.owner) || matchesOwner(options.owner, entry.consumerOwner),
|
||||
);
|
||||
const usedReserved = new Set(reservedImports.map((entry) => entry.subpath));
|
||||
const dormantReserved = new Set<string>(dormantReservedBundledPluginSdkEntrypoints);
|
||||
const unusedReservedSubpaths = reservedBundledPluginSdkEntrypoints
|
||||
.filter(
|
||||
(subpath) =>
|
||||
!usedReserved.has(subpath) &&
|
||||
matchesOwner(options.owner, resolvePluginOwner(subpath, pluginIds)),
|
||||
)
|
||||
.toSorted();
|
||||
const dormantReservedSubpaths = unusedReservedSubpaths
|
||||
.filter((subpath) => dormantReserved.has(subpath))
|
||||
.toSorted();
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
compat: {
|
||||
@@ -273,23 +454,54 @@ function buildReport(): BoundaryReport {
|
||||
pluginSdk: {
|
||||
entrypointCount: pluginSdkEntrypoints.length,
|
||||
reservedCount: reservedBundledPluginSdkEntrypoints.length,
|
||||
dormantReservedCount: dormantReservedBundledPluginSdkEntrypoints.length,
|
||||
supportedBundledFacadeCount: supportedBundledFacadeSdkEntrypoints.length,
|
||||
publicPluginOwnedCount: publicPluginOwnedSdkEntrypoints.length,
|
||||
reservedImports,
|
||||
crossOwnerReservedImports: reservedImports.filter(
|
||||
(entry) => entry.relation === "cross-owner",
|
||||
),
|
||||
unusedReservedSubpaths: reservedBundledPluginSdkEntrypoints
|
||||
.filter((subpath) => !usedReserved.has(subpath))
|
||||
unusedReservedSubpaths,
|
||||
dormantReservedSubpaths,
|
||||
unclassifiedUnusedReservedSubpaths: unusedReservedSubpaths
|
||||
.filter((subpath) => !dormantReserved.has(subpath))
|
||||
.toSorted(),
|
||||
},
|
||||
memoryHostSdk: collectMemoryHostBoundary(files),
|
||||
};
|
||||
}
|
||||
|
||||
function renderText(report: BoundaryReport): string {
|
||||
function renderSummaryText(summary: BoundaryReportSummary): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("Plugin Boundary Report");
|
||||
lines.push(`Plugin Boundary Report${summary.owner ? ` (${summary.owner})` : ""}`);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`compat deprecated=${summary.compat.deprecatedCount} eligibleForRemoval=${summary.compat.eligibleForRemovalCount}`,
|
||||
);
|
||||
lines.push(
|
||||
`plugin-sdk entrypoints=${summary.pluginSdk.entrypointCount} reserved=${summary.pluginSdk.reservedCount} dormantReserved=${summary.pluginSdk.dormantReservedCount}`,
|
||||
);
|
||||
lines.push(
|
||||
` reservedImports=${summary.pluginSdk.reservedImportCount} crossOwnerReservedImports=${summary.pluginSdk.crossOwnerReservedImportCount} unusedReserved=${summary.pluginSdk.unusedReservedCount}`,
|
||||
);
|
||||
lines.push(
|
||||
` dormantUnused=${summary.pluginSdk.dormantReservedCountInUnused} unclassifiedUnused=${summary.pluginSdk.unclassifiedUnusedReservedCount}`,
|
||||
);
|
||||
for (const subpath of summary.pluginSdk.unclassifiedUnusedReservedSubpaths) {
|
||||
lines.push(` unclassified-unused ${subpath}`);
|
||||
}
|
||||
for (const entry of summary.pluginSdk.crossOwnerReservedImports) {
|
||||
lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`);
|
||||
}
|
||||
lines.push(
|
||||
`memory-host-sdk implementation=${summary.memoryHostSdk.implementation} private=${summary.memoryHostSdk.privatePackage} exports=${summary.memoryHostSdk.exportedSubpathCount} sourceBridgeFiles=${summary.memoryHostSdk.sourceBridgeFileCount} coreReferenceFiles=${summary.memoryHostSdk.packageCoreReferenceFileCount}`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderText(report: BoundaryReport, owner?: string): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`Plugin Boundary Report${owner ? ` (${owner})` : ""}`);
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`compat deprecated=${report.compat.deprecatedCount} eligibleForRemoval=${report.compat.eligibleForRemovalCount}`,
|
||||
@@ -301,24 +513,79 @@ function renderText(report: BoundaryReport): string {
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} reserved=${report.pluginSdk.reservedCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`,
|
||||
`plugin-sdk entrypoints=${report.pluginSdk.entrypointCount} reserved=${report.pluginSdk.reservedCount} dormantReserved=${report.pluginSdk.dormantReservedCount} supportedBundledFacade=${report.pluginSdk.supportedBundledFacadeCount} publicPluginOwned=${report.pluginSdk.publicPluginOwnedCount}`,
|
||||
);
|
||||
lines.push(
|
||||
` reservedImports=${report.pluginSdk.reservedImports.length} crossOwnerReservedImports=${report.pluginSdk.crossOwnerReservedImports.length} unusedReserved=${report.pluginSdk.unusedReservedSubpaths.length}`,
|
||||
);
|
||||
lines.push(
|
||||
` dormantUnused=${report.pluginSdk.dormantReservedSubpaths.length} unclassifiedUnused=${report.pluginSdk.unclassifiedUnusedReservedSubpaths.length}`,
|
||||
);
|
||||
for (const subpath of report.pluginSdk.unclassifiedUnusedReservedSubpaths) {
|
||||
lines.push(` unclassified-unused ${subpath}`);
|
||||
}
|
||||
for (const entry of report.pluginSdk.crossOwnerReservedImports) {
|
||||
lines.push(` cross-owner ${entry.file}: ${entry.specifier} owner=${entry.owner ?? "unknown"}`);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push(
|
||||
`memory-host-sdk private=${report.memoryHostSdk.privatePackage} exports=${report.memoryHostSdk.exportedSubpaths.length} sourceBridgeFiles=${report.memoryHostSdk.sourceBridgeFiles.length} coreReferenceFiles=${report.memoryHostSdk.packageCoreReferenceFiles.length}`,
|
||||
`memory-host-sdk implementation=${resolveMemoryHostImplementation(report.memoryHostSdk)} private=${report.memoryHostSdk.privatePackage} exports=${report.memoryHostSdk.exportedSubpaths.length} sourceBridgeFiles=${report.memoryHostSdk.sourceBridgeFiles.length} coreReferenceFiles=${report.memoryHostSdk.packageCoreReferenceFiles.length}`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const report = buildReport();
|
||||
if (process.argv.includes("--json")) {
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${renderText(report)}\n`);
|
||||
function collectFailures(report: BoundaryReport, options: CliOptions): string[] {
|
||||
const failures: string[] = [];
|
||||
if (options.failOnCrossOwner && report.pluginSdk.crossOwnerReservedImports.length > 0) {
|
||||
failures.push(
|
||||
`${report.pluginSdk.crossOwnerReservedImports.length} cross-owner reserved SDK import(s) found`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
options.failOnUnclassifiedUnusedReserved &&
|
||||
report.pluginSdk.unclassifiedUnusedReservedSubpaths.length > 0
|
||||
) {
|
||||
failures.push(
|
||||
`${report.pluginSdk.unclassifiedUnusedReservedSubpaths.length} unused reserved SDK subpath(s) lack dormant classification`,
|
||||
);
|
||||
}
|
||||
if (options.failOnEligibleCompat && report.compat.eligibleForRemovalCount > 0) {
|
||||
failures.push(
|
||||
`${report.compat.eligibleForRemovalCount} compatibility record(s) are due for removal`,
|
||||
);
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
let options: CliOptions;
|
||||
try {
|
||||
options = parseArgs(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n\n${renderHelp()}\n`);
|
||||
process.exitCode = 2;
|
||||
process.exit();
|
||||
}
|
||||
|
||||
if (options.help) {
|
||||
process.stdout.write(`${renderHelp()}\n`);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const report = buildReport(options);
|
||||
const summary = buildSummary(report, options.owner);
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(options.summary ? summary : report, null, 2)}\n`);
|
||||
} else if (options.summary) {
|
||||
process.stdout.write(`${renderSummaryText(summary)}\n`);
|
||||
} else {
|
||||
process.stdout.write(`${renderText(report, options.owner)}\n`);
|
||||
}
|
||||
|
||||
const failures = collectFailures(report, options);
|
||||
if (failures.length > 0) {
|
||||
process.stderr.write(
|
||||
`${failures.map((failure) => `plugin-boundary-report: ${failure}`).join("\n")}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,54 @@ export const reservedBundledPluginSdkEntrypoints = [
|
||||
"zalouser",
|
||||
] as const;
|
||||
|
||||
// Reserved compatibility/helper subpaths with no current tracked imports.
|
||||
// Keeping them classified avoids treating dormant compatibility as unknown debt.
|
||||
export const dormantReservedBundledPluginSdkEntrypoints = [
|
||||
"bluebubbles",
|
||||
"bluebubbles-policy",
|
||||
"browser-cdp",
|
||||
"browser-control-auth",
|
||||
"browser-profiles",
|
||||
"browser-support",
|
||||
"diagnostics-otel",
|
||||
"diagnostics-prometheus",
|
||||
"diffs",
|
||||
"feishu",
|
||||
"feishu-conversation",
|
||||
"feishu-setup",
|
||||
"github-copilot-login",
|
||||
"googlechat",
|
||||
"googlechat-runtime-shared",
|
||||
"irc",
|
||||
"irc-surface",
|
||||
"line",
|
||||
"line-core",
|
||||
"line-runtime",
|
||||
"line-surface",
|
||||
"llm-task",
|
||||
"matrix",
|
||||
"matrix-helper",
|
||||
"matrix-runtime-heavy",
|
||||
"matrix-runtime-surface",
|
||||
"matrix-surface",
|
||||
"matrix-thread-bindings",
|
||||
"mattermost",
|
||||
"mattermost-policy",
|
||||
"memory-lancedb",
|
||||
"msteams",
|
||||
"nextcloud-talk",
|
||||
"nostr",
|
||||
"opencode",
|
||||
"telegram-command-ui",
|
||||
"thread-ownership",
|
||||
"tlon",
|
||||
"twitch",
|
||||
"voice-call",
|
||||
"zalo",
|
||||
"zalo-setup",
|
||||
"zalouser",
|
||||
] as const;
|
||||
|
||||
// Supported SDK facades backed by bundled plugins. These are intentionally public
|
||||
// until they move to generic, plugin-neutral contracts.
|
||||
export const supportedBundledFacadeSdkEntrypoints = [
|
||||
|
||||
@@ -3,6 +3,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
dormantReservedBundledPluginSdkEntrypoints,
|
||||
pluginSdkEntrypoints,
|
||||
publicPluginOwnedSdkEntrypoints,
|
||||
reservedBundledPluginSdkEntrypoints,
|
||||
@@ -487,6 +488,32 @@ function collectCrossOwnerReservedSdkImports(): Array<{
|
||||
return leaks;
|
||||
}
|
||||
|
||||
function collectReservedSdkSubpathImports(): string[] {
|
||||
const imports = new Set<string>();
|
||||
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
|
||||
const importPatterns = [
|
||||
/\b(?:import|export)\b[\s\S]*?\bfrom\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g,
|
||||
/\bimport\s*\(\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']\s*\)/g,
|
||||
/\bvi\.(?:mock|doMock)\s*\(\s*["']openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)["']/g,
|
||||
];
|
||||
|
||||
for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
|
||||
for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
for (const importPattern of importPatterns) {
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const subpath = match[1];
|
||||
if (subpath && reserved.has(subpath)) {
|
||||
imports.add(subpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...imports].toSorted();
|
||||
}
|
||||
|
||||
describe("plugin-sdk package contract guardrails", () => {
|
||||
it("keeps plugin-sdk entrypoint metadata unique", () => {
|
||||
const counts = new Map<string, number>();
|
||||
@@ -508,8 +535,12 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
it("keeps bundled plugin SDK compatibility subpaths explicitly classified", () => {
|
||||
const entrypoints = new Set(pluginSdkEntrypoints);
|
||||
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
|
||||
const dormantReserved = new Set<string>(dormantReservedBundledPluginSdkEntrypoints);
|
||||
const supported = new Set<string>(supportedBundledFacadeSdkEntrypoints);
|
||||
const unknownReserved = [...reserved].filter((entrypoint) => !entrypoints.has(entrypoint));
|
||||
const unknownDormantReserved = [...dormantReserved].filter(
|
||||
(entrypoint) => !reserved.has(entrypoint),
|
||||
);
|
||||
const unknownSupported = [...supported].filter((entrypoint) => !entrypoints.has(entrypoint));
|
||||
const unclassifiedBundledFacades = collectBundledFacadeSdkEntrypoints().filter(
|
||||
(entrypoint) => !reserved.has(entrypoint) && !supported.has(entrypoint),
|
||||
@@ -520,11 +551,13 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
|
||||
expect({
|
||||
unknownReserved,
|
||||
unknownDormantReserved,
|
||||
unknownSupported,
|
||||
unclassifiedBundledFacades,
|
||||
unreservedPrivateSurfaces,
|
||||
}).toEqual({
|
||||
unknownReserved: [],
|
||||
unknownDormantReserved: [],
|
||||
unknownSupported: [],
|
||||
unclassifiedBundledFacades: [],
|
||||
unreservedPrivateSurfaces: [],
|
||||
@@ -637,6 +670,22 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
expect(collectCrossOwnerReservedSdkImports()).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps unused reserved SDK compatibility subpaths classified as dormant", () => {
|
||||
const usedReserved = new Set(collectReservedSdkSubpathImports());
|
||||
const dormantReserved = new Set<string>(dormantReservedBundledPluginSdkEntrypoints);
|
||||
const usedButDormant = [...usedReserved].filter((entrypoint) =>
|
||||
dormantReserved.has(entrypoint),
|
||||
);
|
||||
const unusedUnclassified = reservedBundledPluginSdkEntrypoints.filter(
|
||||
(entrypoint) => !usedReserved.has(entrypoint) && !dormantReserved.has(entrypoint),
|
||||
);
|
||||
|
||||
expect({ usedButDormant, unusedUnclassified }).toEqual({
|
||||
usedButDormant: [],
|
||||
unusedUnclassified: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps generic core poll helpers free of plugin owner names", () => {
|
||||
expect(collectGenericCoreOwnerNameLeaks()).toEqual([]);
|
||||
});
|
||||
|
||||
41
test/scripts/plugin-boundary-report.test.ts
Normal file
41
test/scripts/plugin-boundary-report.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dirname, "../..");
|
||||
|
||||
function runBoundaryReport(...args: string[]): string {
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "scripts/plugin-boundary-report.ts", ...args],
|
||||
{
|
||||
cwd: REPO_ROOT,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 1024 * 1024,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("plugin-boundary-report", () => {
|
||||
it("emits compact CI-safe summary JSON", () => {
|
||||
const output = runBoundaryReport(
|
||||
"--summary",
|
||||
"--json",
|
||||
"--fail-on-cross-owner",
|
||||
"--fail-on-unclassified-unused-reserved",
|
||||
);
|
||||
const summary = JSON.parse(output) as {
|
||||
pluginSdk?: {
|
||||
crossOwnerReservedImportCount?: unknown;
|
||||
unclassifiedUnusedReservedCount?: unknown;
|
||||
};
|
||||
memoryHostSdk?: {
|
||||
implementation?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
expect(summary.pluginSdk?.crossOwnerReservedImportCount).toBe(0);
|
||||
expect(summary.pluginSdk?.unclassifiedUnusedReservedCount).toBe(0);
|
||||
expect(summary.memoryHostSdk?.implementation).toBe("private-core-bridge");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user