From da8576c0bf828740abd0deebfa4c6f0cb34efd51 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 12:35:39 +0100 Subject: [PATCH] test: guard plugin boundary classifications --- docs/plugins/sdk-subpaths.md | 4 +- src/plugin-sdk/entrypoints.ts | 60 ++++++++++++++ ...in-sdk-package-contract-guardrails.test.ts | 82 ++++++++++++++++++- src/polls.ts | 4 +- 4 files changed, 145 insertions(+), 5 deletions(-) diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index 239f92dd0c3..59f3e77421c 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -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` | diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts index ca2a5d1827a..66b10a01fdb 100644 --- a/src/plugin-sdk/entrypoints.ts +++ b/src/plugin-sdk/entrypoints.ts @@ -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`])); diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index 2d7aa41acab..3daf7ed58e6 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -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; optionalDependencies?: Record; @@ -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(reservedBundledPluginSdkEntrypoints); + const supported = new Set(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([]); + }); }); diff --git a/src/polls.ts b/src/polls.ts index c10afd22b64..8244d69aae6 100644 --- a/src/polls.ts +++ b/src/polls.ts @@ -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; };