fix: respect plugin allowlist for bundled deps

This commit is contained in:
Peter Steinberger
2026-04-26 11:00:59 +01:00
parent 93f2d42259
commit d22d6aed16
3 changed files with 88 additions and 5 deletions

View File

@@ -70,6 +70,9 @@ Gateway startup runtime-dependency repair.
Explicit disablement still wins: `plugins.entries.<id>.enabled: false`,
`plugins.deny`, `plugins.enabled: false`, and `channels.<id>.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.<id>.enabled: true`) can
still repair that channel's plugin dependencies.
External plugins and custom load paths must still be installed through
`openclaw plugins install`.

View File

@@ -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" });

View File

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