From d22d6aed16e544b9b2ec0f5f5930758ece497ecd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 11:00:59 +0100 Subject: [PATCH] fix: respect plugin allowlist for bundled deps --- docs/tools/plugin.md | 3 + ...doctor-bundled-plugin-runtime-deps.test.ts | 66 +++++++++++++++++++ src/plugins/bundled-runtime-deps.ts | 24 +++++-- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 6418ddb9577..822130d2a9e 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -70,6 +70,9 @@ Gateway startup runtime-dependency repair. Explicit disablement still wins: `plugins.entries..enabled: false`, `plugins.deny`, `plugins.enabled: false`, and `channels..enabled: false` prevent automatic bundled runtime-dependency repair for that plugin/channel. +A non-empty `plugins.allow` also bounds default-enabled bundled runtime-dependency +repair; explicit bundled channel enablement (`channels..enabled: true`) can +still repair that channel's plugin dependencies. External plugins and custom load paths must still be installed through `openclaw plugins install`. diff --git a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts index d4602c76e2b..006b283005a 100644 --- a/src/commands/doctor-bundled-plugin-runtime-deps.test.ts +++ b/src/commands/doctor-bundled-plugin-runtime-deps.test.ts @@ -286,6 +286,72 @@ describe("doctor bundled plugin runtime deps", () => { expect(result.conflicts).toEqual([]); }); + it("does not report allowlist-excluded default-enabled bundled plugin deps", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); + writeJson(path.join(root, "package.json"), { name: "openclaw" }); + writeJson(path.join(root, "dist", "extensions", "openai", "package.json"), { + dependencies: { + "openai-only": "1.0.0", + }, + }); + writeJson(path.join(root, "dist", "extensions", "openai", "openclaw.plugin.json"), { + id: "openai", + enabledByDefault: true, + configSchema: { type: "object" }, + }); + + const result = scanBundledPluginRuntimeDeps({ + packageRoot: root, + config: { + plugins: { enabled: true, allow: ["browser"] }, + }, + }); + + expect(result.missing).toEqual([]); + expect(result.conflicts).toEqual([]); + }); + + it("lets explicit bundled channel enablement bypass runtime-deps allowlist gating", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); + writeJson(path.join(root, "package.json"), { name: "openclaw" }); + writeBundledChannelPlugin(root, "telegram", { "telegram-only": "1.0.0" }); + + const result = scanBundledPluginRuntimeDeps({ + packageRoot: root, + config: { + plugins: { enabled: true, allow: ["browser"] }, + channels: { + telegram: { enabled: true }, + }, + }, + }); + + expect(result.missing.map((dep) => `${dep.name}@${dep.version}`)).toEqual([ + "telegram-only@1.0.0", + ]); + expect(result.conflicts).toEqual([]); + }); + + it("does not let doctor channel recovery bypass restrictive plugin allowlists", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); + writeJson(path.join(root, "package.json"), { name: "openclaw" }); + writeBundledChannelPlugin(root, "telegram", { "telegram-only": "1.0.0" }); + + const result = scanBundledPluginRuntimeDeps({ + packageRoot: root, + includeConfiguredChannels: true, + config: { + plugins: { enabled: true, allow: ["browser"] }, + channels: { + telegram: { botToken: "123:abc" }, + }, + }, + }); + + expect(result.missing).toEqual([]); + expect(result.conflicts).toEqual([]); + }); + it("repairs missing deps during non-interactive doctor", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-")); writeJson(path.join(root, "package.json"), { name: "openclaw" }); diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index f7a1dbbb6fb..273fc52c6a7 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -907,10 +907,8 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { if (entry?.enabled === false) { return false; } - if (entry?.enabled === true) { - return true; - } let hasExplicitChannelDisable = false; + let hasConfiguredChannel = false; for (const channelId of readBundledPluginChannels(params.pluginDir)) { const normalizedChannelId = normalizeOptionalLowercaseString(channelId); if (!normalizedChannelId) { @@ -932,15 +930,31 @@ function isBundledPluginConfiguredForRuntimeDeps(params: { channelConfig && typeof channelConfig === "object" && !Array.isArray(channelConfig) && - (params.includeConfiguredChannels || - (channelConfig as { enabled?: unknown }).enabled === true) + (channelConfig as { enabled?: unknown }).enabled === true ) { return true; } + if ( + channelConfig && + typeof channelConfig === "object" && + !Array.isArray(channelConfig) && + params.includeConfiguredChannels + ) { + hasConfiguredChannel = true; + } } if (hasExplicitChannelDisable) { return false; } + if (plugins.allow.length > 0 && !plugins.allow.includes(params.pluginId)) { + return false; + } + if (entry?.enabled === true) { + return true; + } + if (hasConfiguredChannel) { + return true; + } return readBundledPluginEnabledByDefault(params.pluginDir); }