fix(plugins): rename bundled allowlist discovery policy

This commit is contained in:
Peter Steinberger
2026-05-04 09:42:37 +01:00
parent 76e0bcd2de
commit 41257a5f6f
20 changed files with 114 additions and 63 deletions

View File

@@ -84,13 +84,13 @@ describe("implicit provider plugin allowlist compatibility", () => {
).toEqual(["kilocode", "moonshot", "openrouter"]);
});
it("respects allowlist for bundled plugins when bundledMode is respect-allow", () => {
it("respects allowlist for bundled plugins when bundledDiscovery is allowlist", () => {
const config = withBundledPluginEnablementCompat({
config: withBundledPluginAllowlistCompat({
config: {
plugins: {
allow: ["openrouter"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
},
},
pluginIds: ["kilocode", "moonshot"],

View File

@@ -161,12 +161,15 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
}));
}
const { collectPluginToolAllowlistWarnings } =
const { collectBundledProviderAllowlistPolicyWarnings, collectPluginToolAllowlistWarnings } =
await import("./doctor/shared/plugin-tool-allowlist-warnings.js");
const pluginToolAllowlistWarnings = collectPluginToolAllowlistWarnings({
cfg: candidate,
env: process.env,
});
const pluginToolAllowlistWarnings = [
...collectPluginToolAllowlistWarnings({
cfg: candidate,
env: process.env,
}),
...collectBundledProviderAllowlistPolicyWarnings({ cfg: candidate }),
];
if (pluginToolAllowlistWarnings.length > 0) {
note(sanitizeDoctorNote(pluginToolAllowlistWarnings.join("\n")), "Doctor warnings");
}

View File

@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import type { PluginManifestRegistry } from "../../../plugins/manifest-registry.js";
import { collectPluginToolAllowlistWarnings } from "./plugin-tool-allowlist-warnings.js";
import {
collectBundledProviderAllowlistPolicyWarnings,
collectPluginToolAllowlistWarnings,
} from "./plugin-tool-allowlist-warnings.js";
const manifestRegistry: PluginManifestRegistry = {
diagnostics: [],
@@ -109,4 +112,29 @@ describe("collectPluginToolAllowlistWarnings", () => {
expect(warnings).toEqual([]);
});
it("warns when restrictive plugins.allow leaves bundled provider discovery in compat mode", () => {
const warnings = collectBundledProviderAllowlistPolicyWarnings({
cfg: {
plugins: { allow: ["telegram"] },
},
});
expect(warnings).toEqual([
expect.stringContaining('set plugins.bundledDiscovery to "allowlist"'),
]);
});
it("does not warn when bundled provider discovery follows the allowlist", () => {
const warnings = collectBundledProviderAllowlistPolicyWarnings({
cfg: {
plugins: {
allow: ["telegram"],
bundledDiscovery: "allowlist",
},
},
});
expect(warnings).toEqual([]);
});
});

View File

@@ -193,3 +193,21 @@ export function collectPluginToolAllowlistWarnings(params: {
return warnings;
}
export function collectBundledProviderAllowlistPolicyWarnings(params: {
cfg: OpenClawConfig;
}): string[] {
if (params.cfg.plugins?.enabled === false) {
return [];
}
const allow = params.cfg.plugins?.allow;
if (!Array.isArray(allow) || allow.length === 0) {
return [];
}
if (params.cfg.plugins?.bundledDiscovery === "allowlist") {
return [];
}
return [
'- plugins.allow is restrictive, but bundled provider discovery is still in legacy compatibility mode. Bundled provider plugins can still appear in runtime provider inventories; set plugins.bundledDiscovery to "allowlist" after confirming omitted bundled providers are intentionally blocked.',
];
}

View File

@@ -24186,12 +24186,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Per-plugin settings keyed by plugin ID including enablement and plugin-specific runtime configuration payloads. Use this for scoped plugin tuning without changing global loader policy.",
},
bundledMode: {
bundledDiscovery: {
type: "string",
enum: ["compat", "respect-allow"],
title: "Bundled Plugin Mode",
enum: ["compat", "allowlist"],
title: "Bundled Plugin Discovery",
description:
'Controls whether bundled plugins bypass plugins.allow on runtime discovery paths. "compat" (default) preserves legacy behavior where bundled provider plugins are force-loaded on every chat turn. "respect-allow" gates bundled plugins by the allowlist the same way third-party plugins are gated.',
'Controls bundled plugin runtime discovery when plugins.allow is configured. "compat" (default) preserves legacy behavior where bundled provider plugins can be force-loaded on every chat turn. "allowlist" gates bundled provider plugins by plugins.allow like third-party plugins.',
},
},
additionalProperties: false,
@@ -28872,9 +28872,9 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Optional allowlist of plugin IDs; when set, only listed plugins are eligible to load. Configured bundled chat channels can still activate their bundled plugin when the channel is explicitly enabled in config. Use this to enforce approved extension inventories in controlled environments.",
tags: ["access"],
},
"plugins.bundledMode": {
label: "Bundled Plugin Mode",
help: 'Controls whether bundled plugins bypass plugins.allow on runtime discovery paths. "compat" (default) preserves legacy behavior where bundled provider plugins are force-loaded on every chat turn. "respect-allow" gates bundled plugins by the allowlist the same way third-party plugins are gated.',
"plugins.bundledDiscovery": {
label: "Bundled Plugin Discovery",
help: 'Controls bundled plugin runtime discovery when plugins.allow is configured. "compat" (default) preserves legacy behavior where bundled provider plugins can be force-loaded on every chat turn. "allowlist" gates bundled provider plugins by plugins.allow like third-party plugins.',
tags: ["advanced"],
},
"plugins.deny": {

View File

@@ -1212,8 +1212,8 @@ export const FIELD_HELP: Record<string, string> = {
'Select the active memory plugin by id, or "none" to disable memory plugins.',
"plugins.slots.contextEngine":
"Selects the active context engine plugin by id so one plugin provides context orchestration behavior.",
"plugins.bundledMode":
'Controls whether bundled plugins bypass plugins.allow on runtime discovery paths. "compat" (default) preserves legacy behavior where bundled provider plugins are force-loaded on every chat turn. "respect-allow" gates bundled plugins by the allowlist the same way third-party plugins are gated.',
"plugins.bundledDiscovery":
'Controls bundled plugin runtime discovery when plugins.allow is configured. "compat" (default) preserves legacy behavior where bundled provider plugins can be force-loaded on every chat turn. "allowlist" gates bundled provider plugins by plugins.allow like third-party plugins.',
"plugins.entries":
"Per-plugin settings keyed by plugin ID including enablement and plugin-specific runtime configuration payloads. Use this for scoped plugin tuning without changing global loader policy.",
"plugins.entries.*.enabled":

View File

@@ -905,7 +905,7 @@ export const FIELD_LABELS: Record<string, string> = {
plugins: "Plugins",
"plugins.enabled": "Enable Plugins",
"plugins.allow": "Plugin Allowlist",
"plugins.bundledMode": "Bundled Plugin Mode",
"plugins.bundledDiscovery": "Bundled Plugin Discovery",
"plugins.deny": "Plugin Denylist",
"plugins.load": "Plugin Loader",
"plugins.load.paths": "Plugin Load Paths",

View File

@@ -52,15 +52,15 @@ export type PluginsConfig = {
/** Optional plugin denylist (plugin ids). */
deny?: string[];
/**
* Controls whether bundled plugins bypass `allow` / `entries` on runtime
* provider discovery paths.
* Controls how bundled plugins participate in runtime provider discovery when
* `allow` is configured.
*
* - `"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.
* - `"allowlist"`: bundled provider plugins are gated by `allow` and
* `entries.<id>.enabled` like third-party plugins.
*/
bundledMode?: "compat" | "respect-allow";
bundledDiscovery?: "compat" | "allowlist";
load?: PluginsLoadConfig;
slots?: PluginSlotsConfig;
entries?: Record<string, PluginEntryConfig>;

View File

@@ -1073,7 +1073,7 @@ export const OpenClawSchema = z
.strict()
.optional(),
entries: z.record(z.string(), PluginEntrySchema).optional(),
bundledMode: z.enum(["compat", "respect-allow"]).optional(),
bundledDiscovery: z.enum(["compat", "allowlist"]).optional(),
})
.strict()
.optional(),

View File

@@ -79,10 +79,10 @@ export function withActivatedPluginIds(params: {
return params.config;
}
const originalAllow = params.config?.plugins?.allow ?? [];
// Empty allowlists are still open; respect-allow only stops compat from widening configured allowlists.
const respectAllow =
params.config?.plugins?.bundledMode === "respect-allow" && originalAllow.length > 0;
const originalAllowSet = respectAllow ? new Set(originalAllow) : undefined;
// Empty allowlists are still open; allowlist mode only stops compat from widening configured allowlists.
const useAllowlistDiscovery =
params.config?.plugins?.bundledDiscovery === "allowlist" && originalAllow.length > 0;
const originalAllowSet = useAllowlistDiscovery ? new Set(originalAllow) : undefined;
const allow = new Set(originalAllow);
const entries = {
...params.config?.plugins?.entries,

View File

@@ -6,7 +6,7 @@ export function withBundledPluginAllowlistCompat(params: {
config: OpenClawConfig | undefined;
pluginIds: readonly string[];
}): OpenClawConfig | undefined {
if (params.config?.plugins?.bundledMode === "respect-allow") {
if (params.config?.plugins?.bundledDiscovery === "allowlist") {
return params.config;
}
const allow = params.config?.plugins?.allow;
@@ -42,8 +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;
const useAllowlistDiscovery = params.config?.plugins?.bundledDiscovery === "allowlist";
const allowSet = useAllowlistDiscovery ? new Set(params.config?.plugins?.allow ?? []) : undefined;
let changed = false;
const nextEntries: Record<string, PluginEntryConfig> = { ...existingEntries };

View File

@@ -593,7 +593,7 @@ describe("resolvePluginProviders", () => {
).toEqual(["legacy-auth-owner"]);
});
it("filters bundled provider plugins by allowlist when bundledMode is respect-allow", () => {
it("filters bundled provider plugins by allowlist when bundledDiscovery is allowlist", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "kilocode",
@@ -619,7 +619,7 @@ describe("resolvePluginProviders", () => {
config: {
plugins: {
allow: ["openrouter"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
},
},
env: {} as NodeJS.ProcessEnv,

View File

@@ -255,7 +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 shouldFilterBundledByAllowlist = params.config?.plugins?.bundledDiscovery === "allowlist";
const normalizedConfig = normalizePluginsConfigWithRegistry(params.config?.plugins, registry);
return listRegistryPluginIds(registry, (plugin) => {
if (
@@ -313,7 +313,7 @@ export function resolveDiscoverableProviderOwnerPluginIds(params: {
includeUntrustedWorkspacePlugins?: boolean;
}): string[] {
const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false;
const shouldFilterBundledByAllowlist = params.config?.plugins?.bundledMode === "respect-allow";
const shouldFilterBundledByAllowlist = params.config?.plugins?.bundledDiscovery === "allowlist";
return resolveProviderOwnerPluginIds({
...params,
isEligible: (plugin, normalizedConfig) =>

View File

@@ -88,7 +88,7 @@ describe("web provider public artifact manifest fallback", () => {
});
});
it("keeps explicit bundled web-search public artifact candidates inside respect-allow", () => {
it("keeps explicit bundled web-search public artifact candidates inside allowlist discovery", () => {
const resolveExplicitWebSearchProviders =
mocks.resolveBundledExplicitWebSearchProvidersFromPublicArtifacts as unknown as {
mockImplementation: (
@@ -105,7 +105,7 @@ describe("web provider public artifact manifest fallback", () => {
config: {
plugins: {
allow: ["fallback-search"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
},
},
onlyPluginIds: ["blocked-search", "fallback-search"],
@@ -117,7 +117,7 @@ describe("web provider public artifact manifest fallback", () => {
});
});
it("keeps manifest bundled web-fetch public artifact candidates inside respect-allow", () => {
it("keeps manifest bundled web-fetch public artifact candidates inside allowlist discovery", () => {
mocks.loadPluginMetadataSnapshot.mockReturnValueOnce({
diagnostics: [],
plugins: [
@@ -140,7 +140,7 @@ describe("web provider public artifact manifest fallback", () => {
config: {
plugins: {
allow: ["fallback-fetch"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
},
},
});

View File

@@ -26,13 +26,13 @@ type BundledCandidateResolution = {
manifestRecords?: readonly PluginManifestRecord[];
};
function filterRespectAllowBundledPluginIds(
function filterAllowlistedBundledPluginIds(
config: PluginLoadOptions["config"] | undefined,
pluginIds: readonly string[],
) {
const allow = config?.plugins?.allow;
if (
config?.plugins?.bundledMode !== "respect-allow" ||
config?.plugins?.bundledDiscovery !== "allowlist" ||
!Array.isArray(allow) ||
allow.length === 0
) {
@@ -57,7 +57,7 @@ function resolveBundledCandidatePluginIds(params: {
: resolveBundledWebFetchResolutionConfig(params).config;
if (params.onlyPluginIds && params.onlyPluginIds.length > 0) {
return {
pluginIds: filterRespectAllowBundledPluginIds(resolvedConfig, [
pluginIds: filterAllowlistedBundledPluginIds(resolvedConfig, [
...new Set(params.onlyPluginIds),
]).toSorted((left, right) => left.localeCompare(right)),
};
@@ -72,7 +72,7 @@ function resolveBundledCandidatePluginIds(params: {
origin: "bundled",
});
return {
pluginIds: filterRespectAllowBundledPluginIds(resolvedConfig, candidates.pluginIds ?? []),
pluginIds: filterAllowlistedBundledPluginIds(resolvedConfig, candidates.pluginIds ?? []),
...(candidates.manifestRecords ? { manifestRecords: candidates.manifestRecords } : {}),
};
}

View File

@@ -482,7 +482,7 @@ describe("resolvePluginWebSearchProviders", () => {
expectScopedWebSearchCandidates(["brave"]);
});
it("keeps respect-allow web-search provider discovery scoped to the configured allowlist", () => {
it("keeps allowlist web-search provider discovery scoped to the configured allowlist", () => {
loadInstalledPluginManifestRegistryMock.mockReturnValueOnce({
plugins: [
createWebSearchManifestRecord({ id: "brave", providerId: "brave" }),
@@ -495,7 +495,7 @@ describe("resolvePluginWebSearchProviders", () => {
config: {
plugins: {
allow: ["brave"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
},
},
bundledAllowlistCompat: true,
@@ -510,7 +510,7 @@ describe("resolvePluginWebSearchProviders", () => {
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["brave"],
bundledMode: "respect-allow",
bundledDiscovery: "allowlist",
entries: { brave: { enabled: true } },
}),
}),