From 7bb69e17a200d50182a3753003dfe611915c568f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 14:28:38 -0700 Subject: [PATCH] fix(config): suggest official plugin installs --- CHANGELOG.md | 1 + src/config/config.plugin-validation.test.ts | 40 ++++++++++++++++++++- src/config/validation.ts | 30 ++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcce92716c8..f722510d911 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. - Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys. - Models/auth: add `openclaw models auth list [--provider ] [--json]` so users can inspect saved per-agent auth profiles without dumping secrets or hitting the old “too many arguments” path. Thanks @vincentkoc. - Control UI/header: show the active agent name in dashboard breadcrumbs without adding the current session key, keeping non-chat views oriented without crowding the topbar. 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 60d4375cff7..45bda33093a 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, @@ -96,6 +100,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; } @@ -1496,6 +1516,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,