Skip to content

Commit b3e42bf

Browse files
authored
fix(plugins): emit actionable install hint for externalized channel plugins (#77502)
Fixes #77483.\n\n- Suggest catalog-backed install commands for missing official external plugins in config validation.\n- Preserve stale/remove wording for non-catalog missing plugins.\n- Add regression coverage for plugins.entries and plugins.allow warnings.\n\nVerification:\n- pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/config/validation.ts src/config/config.plugin-validation.test.ts\n- pnpm test src/config/config.plugin-validation.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts\n- pnpm crabbox:run -- --provider blacksmith-testbox ... pnpm check:changed\n- GitHub CI green on d1b1b10
1 parent 14aa988 commit b3e42bf

3 files changed

Lines changed: 70 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010

1111
### Changes
1212

13+
- 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 <spec>` instead of telling them to remove valid plugin config. (#77483) Thanks @hclsys.
1314
- 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.
1415
- 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)
1516
- 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.

src/config/config.plugin-validation.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,44 @@ describe("config plugin validation", () => {
253253
}
254254
});
255255

256+
it("reports catalog install hints for missing configured official external plugins", async () => {
257+
const res = validateConfigObjectWithPlugins(
258+
{
259+
agents: { list: [{ id: "pi" }] },
260+
plugins: {
261+
entries: { brave: { enabled: true } },
262+
allow: ["brave"],
263+
},
264+
},
265+
{
266+
env: suiteEnv(),
267+
pluginMetadataSnapshot: {
268+
manifestRegistry: {
269+
plugins: [],
270+
diagnostics: [],
271+
},
272+
},
273+
},
274+
);
275+
276+
expect(res.ok).toBe(true);
277+
const message =
278+
"plugin not installed: brave — install the official external plugin with: openclaw plugins install @openclaw/brave-plugin";
279+
expect(res.warnings ?? []).toEqual(
280+
expect.arrayContaining([
281+
{ path: "plugins.entries.brave", message },
282+
{ path: "plugins.allow", message },
283+
]),
284+
);
285+
expect(
286+
(res.warnings ?? []).some(
287+
(warning) =>
288+
(warning.path === "plugins.entries.brave" || warning.path === "plugins.allow") &&
289+
warning.message.includes("remove it from plugins config"),
290+
),
291+
).toBe(false);
292+
});
293+
256294
it.runIf(process.platform !== "win32")(
257295
"reports configured blocked plugins without stale not-found wording",
258296
async () => {
@@ -493,7 +531,7 @@ describe("config plugin validation", () => {
493531
expect(res.warnings ?? []).toContainEqual({
494532
path: "plugins.allow",
495533
message:
496-
"plugin not found: discord (stale config entry ignored; remove it from plugins config)",
534+
"plugin not installed: discord — install the official external plugin with: openclaw plugins install @openclaw/discord",
497535
});
498536
});
499537

src/config/validation.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
import { loadInstalledPluginIndexInstallRecordsSync } from "../plugins/installed-plugin-index-record-reader.js";
1313
import { resolveManifestCommandAliasOwnerInRegistry } from "../plugins/manifest-command-aliases.js";
1414
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
15+
import {
16+
getOfficialExternalPluginCatalogEntry,
17+
resolveOfficialExternalPluginInstall,
18+
} from "../plugins/official-external-plugin-catalog.js";
1519
import {
1620
loadPluginMetadataSnapshot,
1721
type PluginMetadataSnapshot,
@@ -93,6 +97,22 @@ function formatConfigPath(segments: readonly ConfigPathSegment[]): string {
9397
return segments.join(".");
9498
}
9599

100+
function formatMissingOfficialExternalPluginWarning(pluginId: string): string | null {
101+
const catalogEntry = getOfficialExternalPluginCatalogEntry(pluginId);
102+
if (!catalogEntry) {
103+
return null;
104+
}
105+
const install = resolveOfficialExternalPluginInstall(catalogEntry);
106+
const npmSpec = install?.npmSpec?.trim();
107+
const clawhubSpec = install?.clawhubSpec?.trim();
108+
const installSpec =
109+
install?.defaultChoice === "clawhub" ? (clawhubSpec ?? npmSpec) : (npmSpec ?? clawhubSpec);
110+
if (!installSpec) {
111+
return null;
112+
}
113+
return `plugin not installed: ${pluginId} — install the official external plugin with: openclaw plugins install ${installSpec}`;
114+
}
115+
96116
function asJsonSchemaLike(value: unknown): JsonSchemaLike | null {
97117
return value && typeof value === "object" ? (value as JsonSchemaLike) : null;
98118
}
@@ -1441,6 +1461,16 @@ function validateConfigObjectWithPluginsBase(
14411461
}
14421462
return;
14431463
}
1464+
if (opts?.warnOnly) {
1465+
const externalInstallWarning = formatMissingOfficialExternalPluginWarning(pluginId);
1466+
if (externalInstallWarning) {
1467+
warnings.push({
1468+
path,
1469+
message: externalInstallWarning,
1470+
});
1471+
return;
1472+
}
1473+
}
14441474
if (opts?.warnOnly) {
14451475
warnings.push({
14461476
path,

0 commit comments

Comments
 (0)