mirror of https://github.com/openclaw/openclaw.git
fix: preserve bundled channel plugin compat (#58873)
* fix: preserve bundled channel plugin compat * fix: preserve bundled channel plugin compat (#58873) * fix: scope channel plugin compat to bundled plugins (#58873)
This commit is contained in:
parent
2d53ffdec1
commit
fb28b02540
|
|
@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Sandbox/browser: compare browser runtime inspection against `agents.defaults.sandbox.browser.image` so `openclaw sandbox list --browser` stops reporting healthy browser containers as image mismatches. (#58759) Thanks @sandpile.
|
||||
- Exec/approvals: resume the original agent session after an approved async exec finishes, so external completion followups continue the task instead of waiting for a new user turn. (#58860) Thanks @Nanako0129.
|
||||
- Gateway/node pairing: create repair pairing requests when a paired node reconnects with allowlisted commands missing from its approved node record, refresh stale pending repair metadata, and surface paired node command metadata in `nodes status`/`describe` even while the node is offline. Fixes #58824.
|
||||
- Channels/plugins: keep bundled channel plugins loadable from legacy `channels.<id>` config even under restrictive plugin allowlists, and make `openclaw doctor` warn only on real plugin blockers instead of misleading setup guidance. (#58873) Thanks @obviyus
|
||||
|
||||
## 2026.3.31
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,56 @@ describe("doctor config flow", () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
it("shows plugin-blocked guidance instead of first-time Telegram guidance when telegram is explicitly disabled", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
doctorWarnings.some((line) =>
|
||||
line.includes(
|
||||
'channels.telegram: channel is configured, but plugin "telegram" is disabled by plugins.entries.telegram.enabled=false.',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(doctorWarnings.some((line) => line.includes("first-time setup mode"))).toBe(false);
|
||||
});
|
||||
|
||||
it("shows plugin-blocked guidance instead of first-time Telegram guidance when plugins are disabled globally", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
doctorWarnings.some((line) =>
|
||||
line.includes(
|
||||
"channels.telegram: channel is configured, but plugins.enabled=false blocks channel plugins globally.",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(doctorWarnings.some((line) => line.includes("first-time setup mode"))).toBe(false);
|
||||
});
|
||||
|
||||
it("warns on mutable Zalouser group entries when dangerous name matching is disabled", async () => {
|
||||
const doctorWarnings = await collectDoctorWarnings({
|
||||
channels: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import { listPotentialConfiguredChannelIds } from "../../../channels/config-presence.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
resolveEffectiveEnableState,
|
||||
} from "../../../plugins/config-state.js";
|
||||
import { loadPluginManifestRegistry } from "../../../plugins/manifest-registry.js";
|
||||
import { sanitizeForLog } from "../../../terminal/ansi.js";
|
||||
|
||||
export type ChannelPluginBlockerHit = {
|
||||
channelId: string;
|
||||
pluginId: string;
|
||||
reason: "disabled in config" | "plugins disabled";
|
||||
};
|
||||
|
||||
export function scanConfiguredChannelPluginBlockers(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ChannelPluginBlockerHit[] {
|
||||
const configuredChannelIds = new Set(
|
||||
listPotentialConfiguredChannelIds(cfg, env).map((id) => id.trim()),
|
||||
);
|
||||
if (configuredChannelIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pluginsConfig = normalizePluginsConfig(cfg.plugins);
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: cfg,
|
||||
env,
|
||||
});
|
||||
const hits: ChannelPluginBlockerHit[] = [];
|
||||
|
||||
for (const plugin of registry.plugins) {
|
||||
if (plugin.channels.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: plugin.id,
|
||||
origin: plugin.origin,
|
||||
config: pluginsConfig,
|
||||
rootConfig: cfg,
|
||||
enabledByDefault: plugin.enabledByDefault,
|
||||
});
|
||||
if (
|
||||
enableState.enabled ||
|
||||
!enableState.reason ||
|
||||
(enableState.reason !== "disabled in config" && enableState.reason !== "plugins disabled")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const channelId of plugin.channels) {
|
||||
if (!configuredChannelIds.has(channelId)) {
|
||||
continue;
|
||||
}
|
||||
hits.push({
|
||||
channelId,
|
||||
pluginId: plugin.id,
|
||||
reason: enableState.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
function formatReason(hit: ChannelPluginBlockerHit): string {
|
||||
if (hit.reason === "disabled in config") {
|
||||
return `plugin "${sanitizeForLog(hit.pluginId)}" is disabled by plugins.entries.${sanitizeForLog(hit.pluginId)}.enabled=false.`;
|
||||
}
|
||||
if (hit.reason === "plugins disabled") {
|
||||
return `plugins.enabled=false blocks channel plugins globally.`;
|
||||
}
|
||||
return `plugin "${sanitizeForLog(hit.pluginId)}" is not loadable (${sanitizeForLog(hit.reason)}).`;
|
||||
}
|
||||
|
||||
export function collectConfiguredChannelPluginBlockerWarnings(
|
||||
hits: ChannelPluginBlockerHit[],
|
||||
): string[] {
|
||||
return hits.map(
|
||||
(hit) =>
|
||||
`- channels.${sanitizeForLog(hit.channelId)}: channel is configured, but ${formatReason(hit)} Fix plugin enablement before relying on setup guidance for this channel.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function isWarningBlockedByChannelPlugin(
|
||||
warning: string,
|
||||
hits: ChannelPluginBlockerHit[],
|
||||
): boolean {
|
||||
return hits.some((hit) => {
|
||||
const prefix = `channels.${sanitizeForLog(hit.channelId)}`;
|
||||
return warning.includes(`${prefix}:`) || warning.includes(`${prefix}.`);
|
||||
});
|
||||
}
|
||||
|
|
@ -20,6 +20,13 @@ function manifest(id: string): PluginManifestRecord {
|
|||
};
|
||||
}
|
||||
|
||||
function channelManifest(id: string, channelId: string): PluginManifestRecord {
|
||||
return {
|
||||
...manifest(id),
|
||||
channels: [channelId],
|
||||
};
|
||||
}
|
||||
|
||||
describe("doctor preview warnings", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
|
||||
|
|
@ -161,4 +168,66 @@ describe("doctor preview warnings", () => {
|
|||
expect(warnings[0]).toContain("Auto-removal is paused");
|
||||
expect(warnings[0]).toContain('rerun "openclaw doctor --fix"');
|
||||
});
|
||||
|
||||
it("warns when a configured channel plugin is disabled explicitly", () => {
|
||||
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
|
||||
plugins: [channelManifest("telegram", "telegram")],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const warnings = collectDoctorPreviewWarnings({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining(
|
||||
'channels.telegram: channel is configured, but plugin "telegram" is disabled by plugins.entries.telegram.enabled=false.',
|
||||
),
|
||||
]);
|
||||
expect(warnings[0]).not.toContain("first-time setup mode");
|
||||
});
|
||||
|
||||
it("warns when channel plugins are blocked globally", () => {
|
||||
vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({
|
||||
plugins: [channelManifest("telegram", "telegram")],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const warnings = collectDoctorPreviewWarnings({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
botToken: "123:abc",
|
||||
groupPolicy: "allowlist",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
doctorFixCommand: "openclaw doctor --fix",
|
||||
});
|
||||
|
||||
expect(warnings).toEqual([
|
||||
expect.stringContaining(
|
||||
"channels.telegram: channel is configured, but plugins.enabled=false blocks channel plugins globally.",
|
||||
),
|
||||
]);
|
||||
expect(warnings[0]).not.toContain("first-time setup mode");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ import {
|
|||
collectBundledPluginLoadPathWarnings,
|
||||
scanBundledPluginLoadPathMigrations,
|
||||
} from "./bundled-plugin-load-paths.js";
|
||||
import {
|
||||
collectConfiguredChannelPluginBlockerWarnings,
|
||||
isWarningBlockedByChannelPlugin,
|
||||
scanConfiguredChannelPluginBlockers,
|
||||
} from "./channel-plugin-blockers.js";
|
||||
import { scanEmptyAllowlistPolicyWarnings } from "./empty-allowlist-scan.js";
|
||||
import {
|
||||
collectExecSafeBinCoverageWarnings,
|
||||
|
|
@ -40,6 +45,13 @@ export function collectDoctorPreviewWarnings(params: {
|
|||
}): string[] {
|
||||
const warnings: string[] = [];
|
||||
|
||||
const channelPluginBlockerHits = scanConfiguredChannelPluginBlockers(params.cfg, process.env);
|
||||
if (channelPluginBlockerHits.length > 0) {
|
||||
warnings.push(
|
||||
collectConfiguredChannelPluginBlockerWarnings(channelPluginBlockerHits).join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const telegramHits = scanTelegramAllowFromUsernameEntries(params.cfg);
|
||||
if (telegramHits.length > 0) {
|
||||
warnings.push(
|
||||
|
|
@ -94,7 +106,7 @@ export function collectDoctorPreviewWarnings(params: {
|
|||
const emptyAllowlistWarnings = scanEmptyAllowlistPolicyWarnings(params.cfg, {
|
||||
doctorFixCommand: params.doctorFixCommand,
|
||||
extraWarningsForAccount: collectTelegramEmptyAllowlistExtraWarnings,
|
||||
});
|
||||
}).filter((warning) => !isWarningBlockedByChannelPlugin(warning, channelPluginBlockerHits));
|
||||
if (emptyAllowlistWarnings.length > 0) {
|
||||
warnings.push(emptyAllowlistWarnings.map((line) => sanitizeForLog(line)).join("\n"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,25 @@ describe("resolveEffectiveEnableState", () => {
|
|||
});
|
||||
}
|
||||
|
||||
function resolveConfigOriginTelegramState(config: Parameters<typeof normalizePluginsConfig>[0]) {
|
||||
const normalized = normalizePluginsConfig(config);
|
||||
return resolveEffectiveEnableState({
|
||||
id: "telegram",
|
||||
origin: "config",
|
||||
config: normalized,
|
||||
rootConfig: {
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it.each([
|
||||
[{ enabled: true }, { enabled: true }],
|
||||
[{ enabled: true, allow: ["browser"] as string[] }, { enabled: true }],
|
||||
[
|
||||
{
|
||||
enabled: true,
|
||||
|
|
@ -174,6 +191,15 @@ describe("resolveEffectiveEnableState", () => {
|
|||
] as const)("resolves bundled telegram state for %o", (config, expected) => {
|
||||
expect(resolveBundledTelegramState(config)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("does not bypass allowlists for non-bundled plugins that reuse a channel id", () => {
|
||||
expect(
|
||||
resolveConfigOriginTelegramState({
|
||||
enabled: true,
|
||||
allow: ["browser"] as string[],
|
||||
}),
|
||||
).toEqual({ enabled: false, reason: "not in allowlist" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEnableState", () => {
|
||||
|
|
|
|||
|
|
@ -302,8 +302,9 @@ export function resolveEffectiveEnableState(params: {
|
|||
}): { enabled: boolean; reason?: string } {
|
||||
const base = resolveEnableState(params.id, params.origin, params.config, params.enabledByDefault);
|
||||
if (
|
||||
params.origin === "bundled" &&
|
||||
!base.enabled &&
|
||||
base.reason === "bundled (disabled by default)" &&
|
||||
(base.reason === "bundled (disabled by default)" || base.reason === "not in allowlist") &&
|
||||
isBundledChannelEnabledByChannelConfig(params.rootConfig, params.id)
|
||||
) {
|
||||
return { enabled: true };
|
||||
|
|
|
|||
|
|
@ -1014,6 +1014,22 @@ describe("loadOpenClawPlugins", () => {
|
|||
expectTelegramLoaded(registry);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "loads bundled channel plugins when channels.<id>.enabled=true even under restrictive plugins.allow",
|
||||
config: {
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
allow: ["browser"],
|
||||
},
|
||||
} satisfies PluginLoadConfig,
|
||||
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
|
||||
expectTelegramLoaded(registry);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "still respects explicit disable via plugins.entries for bundled channels",
|
||||
config: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue