Skip to content

Commit 192e750

Browse files
committed
fix: repair stale configured channel plugins
1 parent 123a507 commit 192e750

3 files changed

Lines changed: 137 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313
### Fixes
1414

1515
- CLI/plugins: stop treating the non-plugin `auth` command root as a bundled plugin id, so restrictive `plugins.allow` configs no longer tell users to add stale `auth` plugin entries.
16+
- Doctor/plugins: update configured plugin installs whose stale manifests still declare channels without `channelConfigs`, so beta upgrades repair old Discord-style package payloads during `doctor --fix`.
1617
- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc.
1718
- Upgrade/config: validate configured web-search providers and statically suppressed model/provider pairs against the active plugin set at config load, so stale plugin state fails loud before runtime fallback.
1819
- Status/update: resolve beta update-channel checks from the installed version when config still says `stable`, and let `status --deep` reuse live gateway channel credential state instead of warning on command-path-only token misses.

src/commands/doctor/shared/missing-configured-plugin-install.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,101 @@ describe("repairMissingConfiguredPluginInstalls", () => {
791791
expect(result.changes).toEqual(['Repaired missing configured plugin "demo".']);
792792
});
793793

794+
it("updates a configured plugin when its installed manifest lacks channel config descriptors", async () => {
795+
const records = {
796+
discord: {
797+
source: "npm",
798+
spec: "@openclaw/discord",
799+
installPath: "/tmp/openclaw-plugins/discord",
800+
},
801+
};
802+
mocks.loadInstalledPluginIndexInstallRecords.mockResolvedValue(records);
803+
mocks.listChannelPluginCatalogEntries.mockReturnValue([
804+
{
805+
id: "discord",
806+
pluginId: "discord",
807+
meta: { label: "Discord" },
808+
install: {
809+
npmSpec: "@openclaw/discord",
810+
},
811+
},
812+
]);
813+
mocks.loadPluginMetadataSnapshot.mockReturnValue({
814+
plugins: [
815+
{
816+
id: "discord",
817+
channels: ["discord"],
818+
},
819+
],
820+
diagnostics: [
821+
{
822+
level: "warn",
823+
pluginId: "discord",
824+
message:
825+
"channel plugin manifest declares discord without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads",
826+
},
827+
],
828+
});
829+
mocks.updateNpmInstalledPlugins.mockResolvedValue({
830+
changed: true,
831+
config: {
832+
plugins: {
833+
installs: {
834+
discord: {
835+
source: "npm",
836+
spec: "@openclaw/discord",
837+
installPath: "/tmp/openclaw-plugins/discord",
838+
},
839+
},
840+
},
841+
},
842+
outcomes: [
843+
{
844+
pluginId: "discord",
845+
status: "updated",
846+
message: "Updated discord.",
847+
},
848+
],
849+
});
850+
851+
const { repairMissingConfiguredPluginInstalls } =
852+
await import("./missing-configured-plugin-install.js");
853+
const result = await repairMissingConfiguredPluginInstalls({
854+
cfg: {
855+
update: { channel: "beta" },
856+
plugins: {
857+
entries: {
858+
discord: { enabled: true },
859+
},
860+
},
861+
channels: {
862+
discord: { enabled: true },
863+
},
864+
},
865+
env: {},
866+
});
867+
868+
expect(mocks.updateNpmInstalledPlugins).toHaveBeenCalledWith(
869+
expect.objectContaining({
870+
pluginIds: ["discord"],
871+
updateChannel: "beta",
872+
config: expect.objectContaining({
873+
plugins: expect.objectContaining({ installs: records }),
874+
}),
875+
}),
876+
);
877+
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
878+
expect.objectContaining({
879+
discord: expect.objectContaining({ installPath: "/tmp/openclaw-plugins/discord" }),
880+
}),
881+
{ env: {} },
882+
);
883+
expect(result).toEqual({
884+
changes: ['Repaired missing configured plugin "discord".'],
885+
warnings: [],
886+
});
887+
});
888+
794889
it("reinstalls a recorded external web search plugin from provider-only config", async () => {
795890
const records = {
796891
brave: {

src/commands/doctor/shared/missing-configured-plugin-install.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
resolveOfficialExternalPluginInstall,
2121
resolveOfficialExternalPluginLabel,
2222
} from "../../../plugins/official-external-plugin-catalog.js";
23+
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
2324
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
2425
import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
2526
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
@@ -48,6 +49,8 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[]
4849
},
4950
];
5051

52+
const MISSING_CHANNEL_CONFIG_DESCRIPTOR_DIAGNOSTIC = "without channelConfigs metadata";
53+
5154
function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boolean {
5255
return (
5356
result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND ||
@@ -264,6 +267,29 @@ function collectDownloadableInstallCandidates(params: {
264267
);
265268
}
266269

270+
function collectConfiguredPluginIdsWithMissingChannelConfigDescriptors(params: {
271+
snapshot: PluginMetadataSnapshot;
272+
configuredPluginIds: ReadonlySet<string>;
273+
configuredChannelIds: ReadonlySet<string>;
274+
}): Set<string> {
275+
const stalePluginIds = new Set<string>();
276+
const pluginsById = new Map(params.snapshot.plugins.map((plugin) => [plugin.id, plugin]));
277+
for (const diagnostic of params.snapshot.diagnostics) {
278+
const pluginId = diagnostic.pluginId?.trim();
279+
if (!pluginId || !diagnostic.message.includes(MISSING_CHANNEL_CONFIG_DESCRIPTOR_DIAGNOSTIC)) {
280+
continue;
281+
}
282+
const plugin = pluginsById.get(pluginId);
283+
const ownsConfiguredChannel = plugin?.channels.some((channelId) =>
284+
params.configuredChannelIds.has(channelId),
285+
);
286+
if (params.configuredPluginIds.has(pluginId) || ownsConfiguredChannel) {
287+
stalePluginIds.add(pluginId);
288+
}
289+
}
290+
return stalePluginIds;
291+
}
292+
267293
async function installCandidate(params: {
268294
candidate: DownloadableInstallCandidate;
269295
records: Record<string, PluginInstallRecord>;
@@ -405,15 +431,22 @@ async function repairMissingPluginInstalls(params: {
405431
env?: NodeJS.ProcessEnv;
406432
}): Promise<{ changes: string[]; warnings: string[] }> {
407433
const env = params.env ?? process.env;
408-
const knownIds = new Set(
409-
loadManifestMetadataSnapshot({
410-
config: params.cfg,
411-
env,
412-
}).plugins.map((plugin) => plugin.id),
413-
);
434+
const snapshot = loadManifestMetadataSnapshot({
435+
config: params.cfg,
436+
env,
437+
});
438+
const knownIds = new Set(snapshot.plugins.map((plugin) => plugin.id));
439+
const configuredPluginIdsWithStaleDescriptors =
440+
collectConfiguredPluginIdsWithMissingChannelConfigDescriptors({
441+
snapshot,
442+
configuredPluginIds: params.pluginIds,
443+
configuredChannelIds: params.channelIds,
444+
});
414445
const records = await loadInstalledPluginIndexInstallRecords({ env });
415446
const missingRecordedPluginIds = Object.keys(records).filter(
416-
(pluginId) => params.pluginIds.has(pluginId) && !knownIds.has(pluginId),
447+
(pluginId) =>
448+
(params.pluginIds.has(pluginId) && !knownIds.has(pluginId)) ||
449+
configuredPluginIdsWithStaleDescriptors.has(pluginId),
417450
);
418451
const changes: string[] = [];
419452
const warnings: string[] = [];
@@ -429,6 +462,7 @@ async function repairMissingPluginInstalls(params: {
429462
},
430463
},
431464
pluginIds: missingRecordedPluginIds,
465+
updateChannel: params.cfg.update?.channel,
432466
logger: {
433467
warn: (message) => warnings.push(message),
434468
error: (message) => warnings.push(message),

0 commit comments

Comments
 (0)