diff --git a/src/agents/models-config.providers.plugin-allowlist-compat.test.ts b/src/agents/models-config.providers.plugin-allowlist-compat.test.ts index a440b4dbe80..5b2f808be7b 100644 --- a/src/agents/models-config.providers.plugin-allowlist-compat.test.ts +++ b/src/agents/models-config.providers.plugin-allowlist-compat.test.ts @@ -84,6 +84,30 @@ describe("implicit provider plugin allowlist compatibility", () => { ).toEqual(["kilocode", "moonshot", "openrouter"]); }); + it("respects allowlist for bundled plugins when bundledMode is respect-allow", () => { + const config = withBundledPluginEnablementCompat({ + config: withBundledPluginAllowlistCompat({ + config: { + plugins: { + allow: ["openrouter"], + bundledMode: "respect-allow", + }, + }, + pluginIds: ["kilocode", "moonshot"], + }), + pluginIds: ["kilocode", "moonshot"], + }); + + expect( + resolveEnabledProviderPluginIds({ + config, + registry: providerRegistry, + manifestRegistry: providerManifestRegistry, + onlyPluginIds: PROVIDER_PLUGIN_IDS, + }), + ).toEqual(["openrouter"]); + }); + it("still honors explicit plugin denies over compat allowlist injection", () => { const config = withBundledPluginEnablementCompat({ config: withBundledPluginAllowlistCompat({ diff --git a/src/config/types.plugins.ts b/src/config/types.plugins.ts index 0f5818baf37..eb44e59d0fc 100644 --- a/src/config/types.plugins.ts +++ b/src/config/types.plugins.ts @@ -51,6 +51,16 @@ export type PluginsConfig = { allow?: string[]; /** Optional plugin denylist (plugin ids). */ deny?: string[]; + /** + * Controls whether bundled plugins bypass `allow` / `entries` on runtime + * provider discovery paths. + * + * - `"compat"` (default): bundled provider plugins are force-loaded on + * every chat turn regardless of the allowlist (legacy behavior). + * - `"respect-allow"`: bundled provider plugins are gated by `allow` and + * `entries..enabled` the same way third-party plugins are. + */ + bundledMode?: "compat" | "respect-allow"; load?: PluginsLoadConfig; slots?: PluginSlotsConfig; entries?: Record; diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index fa9add9df4c..5753174e772 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -78,7 +78,11 @@ export function withActivatedPluginIds(params: { if (params.pluginIds.length === 0) { return params.config; } - const allow = new Set(params.config?.plugins?.allow ?? []); + const originalAllow = params.config?.plugins?.allow ?? []; + const respectAllow = + params.config?.plugins?.bundledMode === "respect-allow" && originalAllow.length > 0; + const originalAllowSet = respectAllow ? new Set(originalAllow) : undefined; + const allow = new Set(originalAllow); const entries = { ...params.config?.plugins?.entries, }; @@ -87,6 +91,9 @@ export function withActivatedPluginIds(params: { if (!normalized) { continue; } + if (originalAllowSet && !originalAllowSet.has(normalized)) { + continue; + } allow.add(normalized); const existingEntry = entries[normalized]; entries[normalized] = { diff --git a/src/plugins/bundled-compat.ts b/src/plugins/bundled-compat.ts index 1fd096a90a2..33cdecb2700 100644 --- a/src/plugins/bundled-compat.ts +++ b/src/plugins/bundled-compat.ts @@ -6,6 +6,9 @@ export function withBundledPluginAllowlistCompat(params: { config: OpenClawConfig | undefined; pluginIds: readonly string[]; }): OpenClawConfig | undefined { + if (params.config?.plugins?.bundledMode === "respect-allow") { + return params.config; + } const allow = params.config?.plugins?.allow; if (!Array.isArray(allow) || allow.length === 0) { return params.config; @@ -39,6 +42,8 @@ export function withBundledPluginEnablementCompat(params: { }): OpenClawConfig | undefined { const existingEntries = params.config?.plugins?.entries ?? {}; const forcePluginsEnabled = params.config?.plugins?.enabled === false; + const respectAllow = params.config?.plugins?.bundledMode === "respect-allow"; + const allowSet = respectAllow ? new Set(params.config?.plugins?.allow ?? []) : undefined; let changed = false; const nextEntries: Record = { ...existingEntries }; @@ -46,6 +51,9 @@ export function withBundledPluginEnablementCompat(params: { if (existingEntries[pluginId] !== undefined) { continue; } + if (allowSet && !allowSet.has(pluginId)) { + continue; + } nextEntries[pluginId] = { enabled: true }; changed = true; } diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index d936b62ba67..4adb7d30366 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -593,6 +593,75 @@ describe("resolvePluginProviders", () => { ).toEqual(["legacy-auth-owner"]); }); + it("filters bundled provider plugins by allowlist when bundledMode is respect-allow", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "kilocode", + providerIds: ["kilocode"], + origin: "bundled", + enabledByDefault: true, + }), + createManifestProviderPlugin({ + id: "moonshot", + providerIds: ["moonshot"], + origin: "bundled", + enabledByDefault: true, + }), + createManifestProviderPlugin({ + id: "openrouter", + providerIds: ["openrouter"], + origin: "bundled", + enabledByDefault: true, + }), + ]); + + const discovered = resolveDiscoveredProviderPluginIds({ + config: { + plugins: { + allow: ["openrouter"], + bundledMode: "respect-allow", + }, + }, + env: {} as NodeJS.ProcessEnv, + }); + + expect(discovered).toEqual(["openrouter"]); + }); + + it("returns all bundled provider plugins in compat mode (default)", () => { + setManifestPlugins([ + createManifestProviderPlugin({ + id: "kilocode", + providerIds: ["kilocode"], + origin: "bundled", + enabledByDefault: true, + }), + createManifestProviderPlugin({ + id: "moonshot", + providerIds: ["moonshot"], + origin: "bundled", + enabledByDefault: true, + }), + createManifestProviderPlugin({ + id: "openrouter", + providerIds: ["openrouter"], + origin: "bundled", + enabledByDefault: true, + }), + ]); + + const discovered = resolveDiscoveredProviderPluginIds({ + config: { + plugins: { + allow: ["openrouter"], + }, + }, + env: {} as NodeJS.ProcessEnv, + }); + + expect(discovered).toEqual(["kilocode", "moonshot", "openrouter"]); + }); + it("treats explicit empty provider scopes as scoped-empty in provider helpers", () => { expect( resolveEnabledProviderPluginIds({ diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index b9903b5109b..d21282ef8a2 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -255,6 +255,7 @@ export function resolveDiscoveredProviderPluginIds(params: { const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params); const providerSurfacePluginIds = resolveProviderSurfacePluginIdSet({ ...params, registry }); const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false; + const shouldFilterBundledByAllowlist = params.config?.plugins?.bundledMode === "respect-allow"; const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry); return listRegistryPluginIds(registry, (plugin) => { if ( @@ -268,6 +269,7 @@ export function resolveDiscoveredProviderPluginIds(params: { return isProviderPluginEligibleForSetupDiscovery({ plugin, shouldFilterUntrustedWorkspacePlugins, + shouldFilterBundledByAllowlist, normalizedConfig, rootConfig: params.config, }); @@ -277,10 +279,15 @@ export function resolveDiscoveredProviderPluginIds(params: { function isProviderPluginEligibleForSetupDiscovery(params: { plugin: PluginRegistryRecord; shouldFilterUntrustedWorkspacePlugins: boolean; + shouldFilterBundledByAllowlist: boolean; normalizedConfig: NormalizedPluginsConfig; rootConfig?: PluginLoadOptions["config"]; }): boolean { - if (!params.shouldFilterUntrustedWorkspacePlugins || params.plugin.origin !== "workspace") { + if (params.plugin.origin === "workspace") { + if (!params.shouldFilterUntrustedWorkspacePlugins) { + return true; + } + } else if (!params.shouldFilterBundledByAllowlist) { return true; } if ( @@ -306,12 +313,14 @@ export function resolveDiscoverableProviderOwnerPluginIds(params: { includeUntrustedWorkspacePlugins?: boolean; }): string[] { const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false; + const shouldFilterBundledByAllowlist = params.config?.plugins?.bundledMode === "respect-allow"; return resolveProviderOwnerPluginIds({ ...params, isEligible: (plugin, normalizedConfig) => isProviderPluginEligibleForSetupDiscovery({ plugin, shouldFilterUntrustedWorkspacePlugins, + shouldFilterBundledByAllowlist, normalizedConfig, rootConfig: params.config, }),