diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 0b6063bf52d..13b5d9bb6e8 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -36,11 +36,6 @@ import { type OpenClawConfig, type ResolvedDiscordAccount, } from "openclaw/plugin-sdk/discord"; -import { - buildAgentSessionKey, - resolveThreadSessionKeys, - type RoutePeer, -} from "openclaw/plugin-sdk/routing"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import { normalizeMessageChannel } from "../../../src/utils/message-channel.js"; import { diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index 0fd5ca12546..71526f20564 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -26,7 +26,6 @@ import { type ChannelPlugin, type ResolvedIMessageAccount, } from "openclaw/plugin-sdk/imessage"; -import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js"; import { getIMessageRuntime } from "./runtime.js"; diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index a38db8b60d5..bb65502e017 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -4,7 +4,7 @@ import { createScopedAccountConfigAccessors, collectAllowlistProviderRestrictSendersWarnings, } from "openclaw/plugin-sdk/compat"; -import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; +import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core"; import { buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary, diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 592ce5a6331..3b0e347ba24 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -11,7 +11,7 @@ import { buildAgentSessionKey, resolveThreadSessionKeys, type RoutePeer, -} from "openclaw/plugin-sdk/routing"; +} from "openclaw/plugin-sdk/core"; import { buildComputedAccountStatusSnapshot, buildChannelConfigSchema, diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 0b7da81d561..37915fa07df 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -11,7 +11,7 @@ import { buildAgentSessionKey, resolveThreadSessionKeys, type RoutePeer, -} from "openclaw/plugin-sdk/routing"; +} from "openclaw/plugin-sdk/core"; import { buildChannelConfigSchema, buildTokenChannelStatusSummary, diff --git a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts index 01a2eb38c75..dacf30b5623 100644 --- a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts +++ b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts @@ -5,11 +5,16 @@ import { discoverOpenClawPlugins } from "../src/plugins/discovery.js"; // Match exact monolithic-root specifier in any code path: // imports/exports, require/dynamic import, and test mocks (vi.mock/jest.mock). const ROOT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk["']/; +const LEGACY_ROUTING_IMPORT_PATTERN = /["']openclaw\/plugin-sdk\/routing["']/; function hasMonolithicRootImport(content: string): boolean { return ROOT_IMPORT_PATTERN.test(content); } +function hasLegacyRoutingImport(content: string): boolean { + return LEGACY_ROUTING_IMPORT_PATTERN.test(content); +} + function isSourceFile(filePath: string): boolean { if (filePath.endsWith(".d.ts")) { return false; @@ -77,7 +82,8 @@ function main() { filesToCheck.add(sharedFile); } - const offenders: string[] = []; + const monolithicOffenders: string[] = []; + const legacyRoutingOffenders: string[] = []; for (const entryFile of filesToCheck) { let content = ""; try { @@ -86,19 +92,35 @@ function main() { continue; } if (hasMonolithicRootImport(content)) { - offenders.push(entryFile); + monolithicOffenders.push(entryFile); + } + if (hasLegacyRoutingImport(content)) { + legacyRoutingOffenders.push(entryFile); } } - if (offenders.length > 0) { - console.error("Bundled plugin source files must not import monolithic openclaw/plugin-sdk."); - for (const file of offenders.toSorted()) { - const relative = path.relative(process.cwd(), file) || file; - console.error(`- ${relative}`); + if (monolithicOffenders.length > 0 || legacyRoutingOffenders.length > 0) { + if (monolithicOffenders.length > 0) { + console.error("Bundled plugin source files must not import monolithic openclaw/plugin-sdk."); + for (const file of monolithicOffenders.toSorted()) { + const relative = path.relative(process.cwd(), file) || file; + console.error(`- ${relative}`); + } + } + if (legacyRoutingOffenders.length > 0) { + console.error( + "Bundled plugin source files must not import legacy openclaw/plugin-sdk/routing.", + ); + for (const file of legacyRoutingOffenders.toSorted()) { + const relative = path.relative(process.cwd(), file) || file; + console.error(`- ${relative}`); + } + } + if (monolithicOffenders.length > 0 || legacyRoutingOffenders.length > 0) { + console.error( + "Use openclaw/plugin-sdk/ for channel plugins, /core for shared routing and startup surfaces, or /compat for broader internals.", + ); } - console.error( - "Use openclaw/plugin-sdk/ for channel plugins, /core for startup surfaces, or /compat for broader internals.", - ); process.exit(1); } diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index 1a365d71e87..50530a3c051 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -20,7 +20,13 @@ describe("resolvePluginProviders", () => { }); loadPluginManifestRegistryMock.mockReset(); loadPluginManifestRegistryMock.mockReturnValue({ - plugins: [], + plugins: [ + { id: "google", providers: ["google"], origin: "bundled" }, + { id: "kilocode", providers: ["kilocode"], origin: "bundled" }, + { id: "moonshot", providers: ["moonshot"], origin: "bundled" }, + { id: "google-gemini-cli-auth", providers: [], origin: "bundled" }, + { id: "workspace-provider", providers: ["workspace-provider"], origin: "workspace" }, + ], diagnostics: [], }); }); @@ -77,7 +83,7 @@ describe("resolvePluginProviders", () => { config: expect.objectContaining({ plugins: expect.objectContaining({ enabled: true, - allow: expect.arrayContaining(["openai", "moonshot", "zai"]), + allow: expect.arrayContaining(["google", "moonshot"]), }), }), cache: false, @@ -103,6 +109,22 @@ describe("resolvePluginProviders", () => { expect(allow).not.toContain("google-gemini-cli-auth"); }); + it("does not inject non-bundled provider plugin ids into compat allowlists", () => { + resolvePluginProviders({ + config: { + plugins: { + allow: ["openrouter"], + }, + }, + bundledProviderAllowlistCompat: true, + }); + + const call = loadOpenClawPluginsMock.mock.calls.at(-1)?.[0]; + const allow = call?.config?.plugins?.allow; + + expect(allow).not.toContain("workspace-provider"); + }); + it("maps provider ids to owning plugin ids via manifests", () => { loadPluginManifestRegistryMock.mockReturnValue({ plugins: [ diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 97a4dc0b32d..f2a2b4497c9 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -7,39 +7,6 @@ import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { ProviderPlugin } from "./types.js"; const log = createSubsystemLogger("plugins"); -const BUNDLED_PROVIDER_ALLOWLIST_COMPAT_PLUGIN_IDS = [ - "anthropic", - "byteplus", - "cloudflare-ai-gateway", - "copilot-proxy", - "github-copilot", - "google", - "huggingface", - "kilocode", - "kimi-coding", - "minimax", - "mistral", - "modelstudio", - "moonshot", - "nvidia", - "ollama", - "openai", - "opencode", - "opencode-go", - "openrouter", - "qianfan", - "qwen-portal-auth", - "sglang", - "synthetic", - "together", - "venice", - "vercel-ai-gateway", - "volcengine", - "xai", - "vllm", - "xiaomi", - "zai", -] as const; function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { const plugins = config?.plugins; @@ -69,10 +36,11 @@ function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean { function withBundledProviderVitestCompat(params: { config: PluginLoadOptions["config"]; + pluginIds: readonly string[]; env?: PluginLoadOptions["env"]; }): PluginLoadOptions["config"] { const env = params.env ?? process.env; - if (!env.VITEST || hasExplicitPluginConfig(params.config)) { + if (!env.VITEST || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) { return params.config; } @@ -81,7 +49,7 @@ function withBundledProviderVitestCompat(params: { plugins: { ...params.config?.plugins, enabled: true, - allow: [...BUNDLED_PROVIDER_ALLOWLIST_COMPAT_PLUGIN_IDS], + allow: [...params.pluginIds], slots: { ...params.config?.plugins?.slots, memory: "none", @@ -90,6 +58,22 @@ function withBundledProviderVitestCompat(params: { }; } +function resolveBundledProviderCompatPluginIds(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; +}): string[] { + const registry = loadPluginManifestRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + return registry.plugins + .filter((plugin) => plugin.origin === "bundled" && plugin.providers.length > 0) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + export function resolveOwningPluginIdsForProvider(params: { provider: string; config?: PluginLoadOptions["config"]; @@ -126,15 +110,24 @@ export function resolvePluginProviders(params: { activate?: boolean; cache?: boolean; }): ProviderPlugin[] { + const bundledProviderCompatPluginIds = + params.bundledProviderAllowlistCompat || params.bundledProviderVitestCompat + ? resolveBundledProviderCompatPluginIds({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }) + : []; const maybeAllowlistCompat = params.bundledProviderAllowlistCompat ? withBundledPluginAllowlistCompat({ config: params.config, - pluginIds: BUNDLED_PROVIDER_ALLOWLIST_COMPAT_PLUGIN_IDS, + pluginIds: bundledProviderCompatPluginIds, }) : params.config; const config = params.bundledProviderVitestCompat ? withBundledProviderVitestCompat({ config: maybeAllowlistCompat, + pluginIds: bundledProviderCompatPluginIds, env: params.env, }) : maybeAllowlistCompat;