test: guard plugin boundary classifications

This commit is contained in:
Peter Steinberger
2026-04-27 12:35:39 +01:00
parent 7ec97c010c
commit da8576c0bf
4 changed files with 145 additions and 5 deletions

View File

@@ -273,8 +273,8 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| Matrix | `plugin-sdk/matrix`, `plugin-sdk/matrix-helper`, `plugin-sdk/matrix-runtime-heavy`, `plugin-sdk/matrix-runtime-shared`, `plugin-sdk/matrix-runtime-surface`, `plugin-sdk/matrix-surface`, `plugin-sdk/matrix-thread-bindings` | Bundled Matrix helper/runtime surface |
| Line | `plugin-sdk/line`, `plugin-sdk/line-core`, `plugin-sdk/line-runtime`, `plugin-sdk/line-surface` | Bundled LINE helper/runtime surface |
| IRC | `plugin-sdk/irc`, `plugin-sdk/irc-surface` | Bundled IRC helper surface |
| Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu-conversation`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch` | Deprecated bundled channel compatibility/helper seams. New plugins should import generic SDK subpaths or plugin-local barrels. |
| Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` |
| Channel-specific helpers | `plugin-sdk/googlechat`, `plugin-sdk/googlechat-runtime-shared`, `plugin-sdk/zalouser`, `plugin-sdk/bluebubbles`, `plugin-sdk/bluebubbles-policy`, `plugin-sdk/mattermost`, `plugin-sdk/mattermost-policy`, `plugin-sdk/feishu`, `plugin-sdk/feishu-conversation`, `plugin-sdk/feishu-setup`, `plugin-sdk/msteams`, `plugin-sdk/nextcloud-talk`, `plugin-sdk/nostr`, `plugin-sdk/tlon`, `plugin-sdk/twitch`, `plugin-sdk/zalo`, `plugin-sdk/zalo-setup` | Deprecated bundled channel compatibility/helper seams. New plugins should import generic SDK subpaths or plugin-local barrels. |
| Auth/plugin-specific helpers | `plugin-sdk/github-copilot-login`, `plugin-sdk/github-copilot-token`, `plugin-sdk/diagnostics-otel`, `plugin-sdk/diagnostics-prometheus`, `plugin-sdk/diffs`, `plugin-sdk/llm-task`, `plugin-sdk/memory-core`, `plugin-sdk/memory-lancedb`, `plugin-sdk/thread-ownership`, `plugin-sdk/voice-call` | Bundled feature/plugin helper seams; `plugin-sdk/github-copilot-token` currently exports `DEFAULT_COPILOT_API_BASE_URL`, `deriveCopilotApiBaseUrlFromToken`, and `resolveCopilotApiToken` |
</Accordion>
</AccordionGroup>

View File

@@ -4,6 +4,66 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList];
export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index");
export const reservedBundledPluginSdkEntrypoints = [
"bluebubbles",
"bluebubbles-policy",
"browser-cdp",
"browser-config-runtime",
"browser-config-support",
"browser-control-auth",
"browser-node-runtime",
"browser-profiles",
"browser-security-runtime",
"browser-setup-tools",
"browser-support",
"diagnostics-otel",
"diagnostics-prometheus",
"diffs",
"feishu",
"feishu-conversation",
"feishu-setup",
"github-copilot-login",
"github-copilot-token",
"googlechat",
"googlechat-runtime-shared",
"irc",
"irc-surface",
"line",
"line-core",
"line-runtime",
"line-surface",
"llm-task",
"matrix",
"matrix-helper",
"matrix-runtime-heavy",
"matrix-runtime-shared",
"matrix-runtime-surface",
"matrix-surface",
"matrix-thread-bindings",
"mattermost",
"mattermost-policy",
"memory-core",
"memory-lancedb",
"msteams",
"nextcloud-talk",
"nostr",
"thread-ownership",
"tlon",
"twitch",
"voice-call",
"zalo",
"zalo-setup",
"zalouser",
] as const;
export const supportedBundledFacadeSdkEntrypoints = [
"lmstudio",
"lmstudio-runtime",
"memory-core-engine-runtime",
"qa-runner-runtime",
"tts-runtime",
] as const;
/** Map every SDK entrypoint name to its source file path inside the repo. */
export function buildPluginSdkEntrySources(entries: readonly string[] = pluginSdkEntrypoints) {
return Object.fromEntries(entries.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]));

View File

@@ -2,7 +2,11 @@ import { readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { pluginSdkEntrypoints } from "../../plugin-sdk/entrypoints.js";
import {
pluginSdkEntrypoints,
reservedBundledPluginSdkEntrypoints,
supportedBundledFacadeSdkEntrypoints,
} from "../../plugin-sdk/entrypoints.js";
const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const REPO_ROOT = resolve(ROOT_DIR, "..");
@@ -11,6 +15,13 @@ const PUBLIC_CONTRACT_REFERENCE_FILES = [
"src/plugins/contracts/plugin-sdk-subpaths.test.ts",
] as const;
const PLUGIN_SDK_SUBPATH_PATTERN = /openclaw\/plugin-sdk\/([a-z0-9][a-z0-9-]*)\b/g;
const BUNDLED_PLUGIN_FACADE_LOADER_PATTERN =
/\bload(?:Activated)?BundledPluginPublicSurfaceModuleSync\b/;
const PRIVATE_BUNDLED_SDK_SURFACE_PATTERN =
/\b(?:Private helper surface|Narrow plugin-sdk surface for the bundled|Narrow .*runtime exports used by the bundled)\b/i;
const GENERIC_CORE_HELPER_FILES = ["src/polls.ts", "src/poll-params.ts"] as const;
const GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN =
/\b(?:bluebubbles|discord|feishu|googlechat|matrix|mattermost|msteams|slack|telegram|whatsapp|zalo|zalouser)\b/gi;
function collectPluginSdkPackageExports(): string[] {
const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
@@ -46,6 +57,45 @@ function collectPluginSdkSubpathReferences() {
return references;
}
function collectBundledFacadeSdkEntrypoints(): string[] {
const entrypoints: string[] = [];
for (const entrypoint of pluginSdkEntrypoints) {
const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
const source = readFileSync(filePath, "utf8");
if (BUNDLED_PLUGIN_FACADE_LOADER_PATTERN.test(source)) {
entrypoints.push(entrypoint);
}
}
return entrypoints.toSorted();
}
function collectPrivateBundledSdkSurfaceEntrypoints(): string[] {
const entrypoints: string[] = [];
for (const entrypoint of pluginSdkEntrypoints) {
const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
const source = readFileSync(filePath, "utf8");
if (PRIVATE_BUNDLED_SDK_SURFACE_PATTERN.test(source)) {
entrypoints.push(entrypoint);
}
}
return entrypoints.toSorted();
}
function collectGenericCoreOwnerNameLeaks(): Array<{ file: string; match: string }> {
const leaks: Array<{ file: string; match: string }> = [];
for (const file of GENERIC_CORE_HELPER_FILES) {
const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
for (const match of source.matchAll(GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN)) {
const ownerName = match[0];
if (!ownerName) {
continue;
}
leaks.push({ file, match: ownerName });
}
}
return leaks;
}
function readRootPackageJson(): {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
@@ -148,6 +198,32 @@ describe("plugin-sdk package contract guardrails", () => {
expect(collectPluginSdkPackageExports()).toEqual([...pluginSdkEntrypoints].toSorted());
});
it("keeps bundled plugin SDK compatibility subpaths explicitly classified", () => {
const entrypoints = new Set(pluginSdkEntrypoints);
const reserved = new Set<string>(reservedBundledPluginSdkEntrypoints);
const supported = new Set<string>(supportedBundledFacadeSdkEntrypoints);
const unknownReserved = [...reserved].filter((entrypoint) => !entrypoints.has(entrypoint));
const unknownSupported = [...supported].filter((entrypoint) => !entrypoints.has(entrypoint));
const unclassifiedBundledFacades = collectBundledFacadeSdkEntrypoints().filter(
(entrypoint) => !reserved.has(entrypoint) && !supported.has(entrypoint),
);
const unreservedPrivateSurfaces = collectPrivateBundledSdkSurfaceEntrypoints().filter(
(entrypoint) => !reserved.has(entrypoint),
);
expect({
unknownReserved,
unknownSupported,
unclassifiedBundledFacades,
unreservedPrivateSurfaces,
}).toEqual({
unknownReserved: [],
unknownSupported: [],
unclassifiedBundledFacades: [],
unreservedPrivateSurfaces: [],
});
});
it("keeps curated public plugin-sdk references on exported built subpaths", () => {
const entrypoints = new Set(pluginSdkEntrypoints);
const exports = new Set(collectPluginSdkPackageExports());
@@ -192,4 +268,8 @@ describe("plugin-sdk package contract guardrails", () => {
it("keeps extension sources on public sdk or local package seams", () => {
expect(collectExtensionCoreImportLeaks()).toEqual([]);
});
it("keeps generic core poll helpers free of plugin owner names", () => {
expect(collectGenericCoreOwnerNameLeaks()).toEqual([]);
});
});

View File

@@ -4,12 +4,12 @@ export type PollInput = {
maxSelections?: number;
/**
* Poll duration in seconds.
* Channel-specific limits apply (e.g. Telegram open_period is 5-600s).
* Channel-specific limits apply in each owning plugin.
*/
durationSeconds?: number;
/**
* Poll duration in hours.
* Used by channels that model duration in hours (e.g. Discord).
* Used by channels that model duration in hours.
*/
durationHours?: number;
};