fix(plugins): add bundledMode to gate runtime provider discovery by allowlist

When plugins.bundledMode is set to "respect-allow", runtime provider
discovery paths honor plugins.allow for bundled plugins instead of
force-loading all providers. Default "compat" preserves existing behavior.

Closes #75575
This commit is contained in:
dougbtv
2026-05-02 09:29:18 -04:00
committed by Peter Steinberger
parent 81035e651b
commit f738663c79
6 changed files with 129 additions and 2 deletions

View File

@@ -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({

View File

@@ -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.<id>.enabled` the same way third-party plugins are.
*/
bundledMode?: "compat" | "respect-allow";
load?: PluginsLoadConfig;
slots?: PluginSlotsConfig;
entries?: Record<string, PluginEntryConfig>;

View File

@@ -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] = {

View File

@@ -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<string, PluginEntryConfig> = { ...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;
}

View File

@@ -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({

View File

@@ -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,
}),