mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:40:42 +00:00
fix: tolerate stale channel plugin config
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/tools: ignore volatile `exec` runtime metadata when comparing tool-loop outcomes, so enabled loop detection can stop repeated identical shell-command results instead of resetting on duration, PID, session, or cwd changes. Fixes #34574; supersedes #41502. Thanks @gucasbrg and @Zcg2021.
|
||||
- Agents/fallback: classify internal live-session model switch conflicts as unknown fallback failures instead of provider overloads, preventing local vLLM endpoints from receiving misleading overloaded cooldowns. Refs #63229. Thanks @clawdia-lobster.
|
||||
- Discord: let thread sessions inherit the parent channel's session-level `/model` override as a model-only fallback without enabling parent transcript inheritance. Fixes #72755. Thanks @solavrc.
|
||||
- Gateway/plugins: skip stale configured channels whose matching plugin is no longer discoverable, point cleanup at `openclaw doctor --fix`, and keep unrelated channel typos fatal so one missing channel plugin no longer crash-loops the Gateway. Fixes #53311. Thanks @futhgar.
|
||||
- Control UI: keep session-specific assistant identity loads authoritative after WebSocket connect, so non-main agent chat sessions do not show the main agent name in the header after bootstrap refreshes. Fixes #72776. Thanks @rockytian-top.
|
||||
- Agents/Qwen: preserve exact custom `modelstudio` provider configs with foreign `api` owners so explicit OpenAI-compatible Model Studio endpoints no longer get normalized into the bundled Qwen plugin path. Fixes #64483. Thanks @FiredMosquito831.
|
||||
- MCP/bundle-mcp: normalize CLI-native `type: "http"` MCP server entries to OpenClaw `transport: "streamable-http"` on save, repair existing configs with doctor, and keep embedded Pi from falling back to legacy SSE GET-first startup for those servers. Fixes #72757. Thanks @Studioscale.
|
||||
|
||||
@@ -61,6 +61,12 @@ If config is invalid, install normally fails closed and points you at
|
||||
`openclaw doctor --fix`. The only recovery exception is a narrow bundled-plugin
|
||||
reinstall path for plugins that opt into
|
||||
`openclaw.install.allowInvalidConfigRecovery`.
|
||||
When a channel config references a plugin that is no longer discoverable but the
|
||||
same stale plugin id remains in plugin config or install records, Gateway startup
|
||||
logs warnings and skips that channel instead of blocking every other channel.
|
||||
Run `openclaw doctor --fix` to remove the stale channel/plugin entries; unknown
|
||||
channel keys without stale-plugin evidence still fail validation so typos stay
|
||||
visible.
|
||||
|
||||
Packaged OpenClaw installs do not eagerly install every bundled plugin's
|
||||
runtime dependency tree. When a bundled OpenClaw-owned plugin is active from
|
||||
|
||||
@@ -266,6 +266,104 @@ describe("config plugin validation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("warns instead of failing for stale channel config backed by missing plugin refs", async () => {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
channels: {
|
||||
"missing-chat": { token: "stale" },
|
||||
},
|
||||
plugins: {
|
||||
allow: ["missing-chat"],
|
||||
entries: { "missing-chat": { enabled: true } },
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
expect(res.warnings).toContainEqual({
|
||||
path: "channels.missing-chat",
|
||||
message:
|
||||
"unknown channel id: missing-chat (stale channel plugin config ignored; run openclaw doctor --fix to remove stale config, or install the plugin)",
|
||||
});
|
||||
expect(res.warnings).toContainEqual({
|
||||
path: "plugins.allow",
|
||||
message:
|
||||
"plugin not found: missing-chat (stale config entry ignored; remove it from plugins config)",
|
||||
});
|
||||
expect(res.warnings).toContainEqual({
|
||||
path: "plugins.entries.missing-chat",
|
||||
message:
|
||||
"plugin not found: missing-chat (stale config entry ignored; remove it from plugins config)",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unknown channel typos fatal when there is no stale plugin evidence", async () => {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
channels: {
|
||||
telegarm: { botToken: "typo" },
|
||||
},
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
if (res.ok) {
|
||||
return;
|
||||
}
|
||||
expect(res.issues).toContainEqual({
|
||||
path: "channels.telegarm",
|
||||
message: "unknown channel id: telegarm",
|
||||
});
|
||||
expect(res.warnings).not.toContainEqual(expect.objectContaining({ path: "channels.telegarm" }));
|
||||
});
|
||||
|
||||
it("uses persisted installed-plugin records as stale channel evidence", async () => {
|
||||
const installedPluginIndexPath = path.join(suiteHome, ".openclaw", "plugins", "installs.json");
|
||||
await mkdirSafe(path.dirname(installedPluginIndexPath));
|
||||
await fs.writeFile(
|
||||
installedPluginIndexPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
installRecords: {
|
||||
"missing-sms": {
|
||||
source: "npm",
|
||||
spec: "missing-sms@1.0.0",
|
||||
installedAt: "2026-04-12T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
try {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
channels: {
|
||||
"missing-sms": { token: "stale" },
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
expect(res.warnings).toContainEqual({
|
||||
path: "channels.missing-sms",
|
||||
message:
|
||||
"unknown channel id: missing-sms (stale channel plugin config ignored; run openclaw doctor --fix to remove stale config, or install the plugin)",
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(installedPluginIndexPath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("warns with actionable guidance when a runtime command name is used in plugins.allow", async () => {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/ids.js";
|
||||
import { withBundledPluginAllowlistCompat } from "../plugins/bundled-compat.js";
|
||||
import {
|
||||
normalizePluginsConfig,
|
||||
normalizePluginId,
|
||||
resolveEffectivePluginActivationState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../plugins/config-state.js";
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
collectRelevantDoctorPluginIdsForTouchedPaths,
|
||||
listPluginDoctorLegacyConfigRules,
|
||||
} from "../plugins/doctor-contract-registry.js";
|
||||
import { loadInstalledPluginIndexInstallRecordsSync } from "../plugins/installed-plugin-index-record-reader.js";
|
||||
import { resolveManifestCommandAliasOwnerInRegistry } from "../plugins/manifest-command-aliases.js";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
@@ -926,6 +928,54 @@ function validateConfigObjectWithPluginsBase(
|
||||
let channelsCloned = false;
|
||||
let pluginsCloned = false;
|
||||
let pluginEntriesCloned = false;
|
||||
let installedPluginRecordIds: Set<string> | undefined;
|
||||
|
||||
const ensureInstalledPluginRecordIds = (): Set<string> => {
|
||||
if (installedPluginRecordIds) {
|
||||
return installedPluginRecordIds;
|
||||
}
|
||||
try {
|
||||
installedPluginRecordIds = new Set(
|
||||
Object.keys(loadInstalledPluginIndexInstallRecordsSync({ env: opts.env })).map(
|
||||
normalizePluginId,
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
installedPluginRecordIds = new Set();
|
||||
}
|
||||
return installedPluginRecordIds;
|
||||
};
|
||||
|
||||
const hasStalePluginEvidenceForUnknownChannel = (channelId: string): boolean => {
|
||||
const normalizedChannelId = normalizePluginId(channelId);
|
||||
if (!normalizedChannelId || ensureKnownIds().has(normalizedChannelId)) {
|
||||
return false;
|
||||
}
|
||||
const pluginConfig = config.plugins;
|
||||
if (
|
||||
Array.isArray(pluginConfig?.allow) &&
|
||||
pluginConfig.allow.some((pluginId) => normalizePluginId(pluginId) === normalizedChannelId)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isRecord(pluginConfig?.entries) &&
|
||||
Object.keys(pluginConfig.entries).some(
|
||||
(pluginId) => normalizePluginId(pluginId) === normalizedChannelId,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
isRecord(pluginConfig?.installs) &&
|
||||
Object.keys(pluginConfig.installs).some(
|
||||
(pluginId) => normalizePluginId(pluginId) === normalizedChannelId,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return ensureInstalledPluginRecordIds().has(normalizedChannelId);
|
||||
};
|
||||
|
||||
const replaceChannelConfig = (channelId: string, nextValue: unknown) => {
|
||||
if (!channelsCloned) {
|
||||
@@ -983,10 +1033,18 @@ function validateConfigObjectWithPluginsBase(
|
||||
}
|
||||
}
|
||||
if (!allowedChannels.has(trimmed)) {
|
||||
issues.push({
|
||||
const issue = {
|
||||
path: `channels.${trimmed}`,
|
||||
message: `unknown channel id: ${trimmed}`,
|
||||
});
|
||||
};
|
||||
if (hasStalePluginEvidenceForUnknownChannel(trimmed)) {
|
||||
warnings.push({
|
||||
...issue,
|
||||
message: `${issue.message} (stale channel plugin config ignored; run openclaw doctor --fix to remove stale config, or install the plugin)`,
|
||||
});
|
||||
} else {
|
||||
issues.push(issue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user