diff --git a/CHANGELOG.md b/CHANGELOG.md index ed871041fbd..dae1b57fede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Plugins/migration: emit catalog-backed install hints when `plugins.entries` or `plugins.allow` references an official external plugin that is not installed, so upgraded configs point operators to `openclaw plugins install ` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys. - Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure. - Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532) - Config/plugin auto-enable: prefer the claiming plugin manifest id over a built-in channel alias when auto-allowlisting a configured channel, so WeCom/Yuanbao-style aliases resolve to the installed plugin id. Thanks @Beandon13. diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index 0201f010ca1..d10fc9e1013 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -253,6 +253,44 @@ describe("config plugin validation", () => { } }); + it("reports catalog install hints for missing configured official external plugins", async () => { + const res = validateConfigObjectWithPlugins( + { + agents: { list: [{ id: "pi" }] }, + plugins: { + entries: { brave: { enabled: true } }, + allow: ["brave"], + }, + }, + { + env: suiteEnv(), + pluginMetadataSnapshot: { + manifestRegistry: { + plugins: [], + diagnostics: [], + }, + }, + }, + ); + + expect(res.ok).toBe(true); + const message = + "plugin not installed: brave — install the official external plugin with: openclaw plugins install @openclaw/brave-plugin"; + expect(res.warnings ?? []).toEqual( + expect.arrayContaining([ + { path: "plugins.entries.brave", message }, + { path: "plugins.allow", message }, + ]), + ); + expect( + (res.warnings ?? []).some( + (warning) => + (warning.path === "plugins.entries.brave" || warning.path === "plugins.allow") && + warning.message.includes("remove it from plugins config"), + ), + ).toBe(false); + }); + it.runIf(process.platform !== "win32")( "reports configured blocked plugins without stale not-found wording", async () => { @@ -493,7 +531,7 @@ describe("config plugin validation", () => { expect(res.warnings ?? []).toContainEqual({ path: "plugins.allow", message: - "plugin not found: discord (stale config entry ignored; remove it from plugins config)", + "plugin not installed: discord — install the official external plugin with: openclaw plugins install @openclaw/discord", }); }); diff --git a/src/config/validation.ts b/src/config/validation.ts index ac60dfd8140..debc4f1526c 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -12,6 +12,10 @@ import { 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 { + getOfficialExternalPluginCatalogEntry, + resolveOfficialExternalPluginInstall, +} from "../plugins/official-external-plugin-catalog.js"; import { loadPluginMetadataSnapshot, type PluginMetadataSnapshot, @@ -93,6 +97,22 @@ function formatConfigPath(segments: readonly ConfigPathSegment[]): string { return segments.join("."); } +function formatMissingOfficialExternalPluginWarning(pluginId: string): string | null { + const catalogEntry = getOfficialExternalPluginCatalogEntry(pluginId); + if (!catalogEntry) { + return null; + } + const install = resolveOfficialExternalPluginInstall(catalogEntry); + const npmSpec = install?.npmSpec?.trim(); + const clawhubSpec = install?.clawhubSpec?.trim(); + const installSpec = + install?.defaultChoice === "clawhub" ? (clawhubSpec ?? npmSpec) : (npmSpec ?? clawhubSpec); + if (!installSpec) { + return null; + } + return `plugin not installed: ${pluginId} — install the official external plugin with: openclaw plugins install ${installSpec}`; +} + function asJsonSchemaLike(value: unknown): JsonSchemaLike | null { return value && typeof value === "object" ? (value as JsonSchemaLike) : null; } @@ -1441,6 +1461,16 @@ function validateConfigObjectWithPluginsBase( } return; } + if (opts?.warnOnly) { + const externalInstallWarning = formatMissingOfficialExternalPluginWarning(pluginId); + if (externalInstallWarning) { + warnings.push({ + path, + message: externalInstallWarning, + }); + return; + } + } if (opts?.warnOnly) { warnings.push({ path,