fix: preserve configured plugins in allowlist

This commit is contained in:
Peter Steinberger
2026-04-11 02:08:10 +01:00
parent 202f80792e
commit dc008f956c
2 changed files with 118 additions and 9 deletions

View File

@@ -57,7 +57,7 @@ describe("applyPluginAutoEnable core", () => {
expect(result.autoEnabledReasons).toEqual({});
});
it("auto-enables built-in channels without appending to plugins.allow", () => {
it("auto-enables built-in channels and preserves them in restrictive plugins.allow", () => {
const result = applyPluginAutoEnable({
config: {
channels: { slack: { botToken: "x" } },
@@ -68,7 +68,7 @@ describe("applyPluginAutoEnable core", () => {
expect(result.config.channels?.slack?.enabled).toBe(true);
expect(result.config.plugins?.entries?.slack).toBeUndefined();
expect(result.config.plugins?.allow).toEqual(["telegram"]);
expect(result.config.plugins?.allow).toEqual(["telegram", "slack"]);
expect(result.autoEnabledReasons).toEqual({
slack: ["slack configured"],
});
@@ -285,7 +285,7 @@ describe("applyPluginAutoEnable core", () => {
expect(validateConfigObject(result.config).ok).toBe(true);
});
it("does not append built-in WhatsApp to plugins.allow during auto-enable", () => {
it("appends built-in WhatsApp to restrictive plugins.allow during auto-enable", () => {
const result = applyPluginAutoEnable({
config: {
channels: {
@@ -301,10 +301,52 @@ describe("applyPluginAutoEnable core", () => {
});
expect(result.config.channels?.whatsapp?.enabled).toBe(true);
expect(result.config.plugins?.allow).toEqual(["telegram"]);
expect(result.config.plugins?.allow).toEqual(["telegram", "whatsapp"]);
expect(validateConfigObject(result.config).ok).toBe(true);
});
it("preserves configured plugin entries in restrictive plugins.allow", () => {
const result = applyPluginAutoEnable({
config: {
plugins: {
allow: ["glueclaw"],
entries: {
discord: {
config: {
token: "x",
},
},
},
},
},
env: makeIsolatedEnv(),
});
expect(result.config.plugins?.allow).toEqual(["glueclaw", "discord"]);
expect(result.changes).toContain("discord plugin config present, added to plugin allowlist.");
});
it("does not preserve stale configured plugin entries in restrictive plugins.allow", () => {
const result = applyPluginAutoEnable({
config: {
plugins: {
allow: ["glueclaw"],
entries: {
"missing-plugin": {
config: {
token: "x",
},
},
},
},
},
env: makeIsolatedEnv(),
});
expect(result.config.plugins?.allow).toEqual(["glueclaw"]);
expect(result.changes).toEqual([]);
});
it("does not re-emit built-in auto-enable changes when rerun with plugins.allow set", () => {
const first = applyPluginAutoEnable({
config: {

View File

@@ -256,8 +256,16 @@ function hasConfiguredPluginConfigEntry(cfg: OpenClawConfig): boolean {
);
}
function hasPluginEntries(cfg: OpenClawConfig): boolean {
const entries = cfg.plugins?.entries;
return !!entries && typeof entries === "object" && Object.keys(entries).length > 0;
}
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig): boolean {
const pluginEntries = cfg.plugins?.entries;
if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.length > 0 && hasPluginEntries(cfg)) {
return true;
}
if (
pluginEntries &&
Object.values(pluginEntries).some((entry) => isRecord(entry) && isRecord(entry.config))
@@ -292,6 +300,9 @@ export function configMayNeedPluginAutoEnable(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): boolean {
if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.length > 0 && hasPluginEntries(cfg)) {
return true;
}
if (hasConfiguredPluginConfigEntry(cfg)) {
return true;
}
@@ -500,6 +511,59 @@ function registerPluginEntry(cfg: OpenClawConfig, pluginId: string): OpenClawCon
};
}
function hasMaterialPluginEntryConfig(entry: unknown): boolean {
if (!isRecord(entry)) {
return false;
}
return (
entry.enabled === true ||
isRecord(entry.config) ||
isRecord(entry.hooks) ||
isRecord(entry.subagent) ||
entry.apiKey !== undefined ||
entry.env !== undefined
);
}
function isKnownPluginId(pluginId: string, manifestRegistry: PluginManifestRegistry): boolean {
if (normalizeChatChannelId(pluginId)) {
return true;
}
return manifestRegistry.plugins.some((plugin) => plugin.id === pluginId);
}
function materializeConfiguredPluginEntryAllowlist(params: {
config: OpenClawConfig;
changes: string[];
manifestRegistry: PluginManifestRegistry;
}): OpenClawConfig {
let next = params.config;
const allow = next.plugins?.allow;
const entries = next.plugins?.entries;
if (!Array.isArray(allow) || allow.length === 0 || !entries || typeof entries !== "object") {
return next;
}
for (const pluginId of Object.keys(entries).toSorted((left, right) =>
left.localeCompare(right),
)) {
const entry = entries[pluginId];
if (
!hasMaterialPluginEntryConfig(entry) ||
isPluginDenied(next, pluginId) ||
isPluginExplicitlyDisabled(next, pluginId) ||
allow.includes(pluginId) ||
!isKnownPluginId(pluginId, params.manifestRegistry)
) {
continue;
}
next = ensurePluginAllowlisted(next, pluginId);
params.changes.push(`${pluginId} plugin config present, added to plugin allowlist.`);
}
return next;
}
function formatAutoEnableChange(entry: PluginAutoEnableCandidate): string {
let reason = resolvePluginAutoEnableCandidateReason(entry).trim();
const channelId = normalizeChatChannelId(entry.pluginId);
@@ -557,8 +621,7 @@ export function materializePluginAutoEnableCandidatesInternal(params: {
}
const allow = next.plugins?.allow;
const allowMissing =
builtInChannelId == null && Array.isArray(allow) && !allow.includes(entry.pluginId);
const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId);
const alreadyEnabled =
builtInChannelId != null
? isBuiltInChannelAlreadyEnabled(next, builtInChannelId)
@@ -568,9 +631,7 @@ export function materializePluginAutoEnableCandidatesInternal(params: {
}
next = registerPluginEntry(next, entry.pluginId);
if (!builtInChannelId) {
next = ensurePluginAllowlisted(next, entry.pluginId);
}
next = ensurePluginAllowlisted(next, entry.pluginId);
const reason = resolvePluginAutoEnableCandidateReason(entry);
autoEnabledReasons.set(entry.pluginId, [
...(autoEnabledReasons.get(entry.pluginId) ?? []),
@@ -579,6 +640,12 @@ export function materializePluginAutoEnableCandidatesInternal(params: {
changes.push(formatAutoEnableChange(entry));
}
next = materializeConfiguredPluginEntryAllowlist({
config: next,
changes,
manifestRegistry: params.manifestRegistry,
});
const autoEnabledReasonRecord: Record<string, string[]> = Object.create(null);
for (const [pluginId, reasons] of autoEnabledReasons) {
if (!isBlockedObjectKey(pluginId)) {