From c0ec58f4b6906ad06fd4c6f265d43be053b2d193 Mon Sep 17 00:00:00 2001 From: Shakker Date: Tue, 28 Apr 2026 16:48:15 +0100 Subject: [PATCH] fix: preserve runtime kind install fallback --- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/plugins/manifest.md | 1 + src/cli/plugins-cli.install.test.ts | 1 - src/cli/plugins-command-helpers.ts | 31 +++- src/cli/plugins-install-persist.test.ts | 164 ++++++++++++++++++ src/plugin-sdk/plugin-entry.ts | 5 + src/plugin-sdk/provider-entry.ts | 5 + src/plugins/types.ts | 6 + 8 files changed, 212 insertions(+), 5 deletions(-) diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 92c1fcae9f1..8a233743d41 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -9a688c953f0108f85f58c173e79c28363d846a592130abec04cafbcabbb22dcc plugin-sdk-api-baseline.json -010252e56202abde0816787588239c41b4bfb710b930a5454848a5ae76ad6dae plugin-sdk-api-baseline.jsonl +a55e260675c0f02be5fe0444138683f6a0cb96d6bc6f0fa2b7026df2ea8165b2 plugin-sdk-api-baseline.json +d4e9dea5aaa8a63d0f609acab2ac2962ee57ffa8bc6e5dd313a00f32cfd54b40 plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 963163be27c..97a6429f534 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -1136,6 +1136,7 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s - `channels`, `providers`, `cliBackends`, and `skills` can all be omitted when a plugin does not need them. - `providerDiscoveryEntry` must stay lightweight and should not import broad runtime code; use it for static provider catalog metadata or narrow discovery descriptors, not request-time execution. - Exclusive plugin kinds are selected through `plugins.slots.*`: `kind: "memory"` via `plugins.slots.memory`, `kind: "context-engine"` via `plugins.slots.contextEngine` (default `legacy`). +- Declare exclusive plugin kind in this manifest. Runtime-entry `OpenClawPluginDefinition.kind` is deprecated and remains only as a compatibility fallback for older plugins. - Env-var metadata (`setup.providers[].envVars`, deprecated `providerAuthEnvVars`, and `channelEnvVars`) is declarative only. Status, audit, cron delivery validation, and other read-only surfaces still apply plugin trust and effective activation policy before treating an env var as configured. - For runtime wizard metadata that requires provider code, see [Provider runtime hooks](/plugins/architecture-internals#provider-runtime-hooks). - If your plugin depends on native modules, document the build steps and any package-manager allowlist requirements (for example, pnpm `allow-build-scripts` + `pnpm rebuild `). diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index 142d3014ce1..0d6754fd0cd 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -6,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { applyExclusiveSlotSelection, - buildPluginDiagnosticsReport, buildPluginSnapshotReport, clearPluginManifestRegistryCache, enablePluginInConfig, diff --git a/src/cli/plugins-command-helpers.ts b/src/cli/plugins-command-helpers.ts index ba2da008b87..e5d62119173 100644 --- a/src/cli/plugins-command-helpers.ts +++ b/src/cli/plugins-command-helpers.ts @@ -1,8 +1,8 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js"; import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js"; -import { applyExclusiveSlotSelection } from "../plugins/slots.js"; -import { buildPluginSnapshotReport } from "../plugins/status.js"; +import { applyExclusiveSlotSelection, slotKeysForPluginKind } from "../plugins/slots.js"; +import { buildPluginDiagnosticsReport, buildPluginSnapshotReport } from "../plugins/status.js"; import { defaultRuntime } from "../runtime.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { theme } from "../terminal/theme.js"; @@ -44,6 +44,33 @@ export function applySlotSelectionForPlugin( if (!plugin) { return { config, warnings: [] }; } + if ( + plugin.kind && + slotKeysForPluginKind(plugin.kind).length > 0 && + report.plugins.some((entry) => entry.id !== plugin.id && !entry.kind) + ) { + const runtimeReport = buildPluginDiagnosticsReport({ config }); + const result = applyExclusiveSlotSelection({ + config, + selectedId: plugin.id, + selectedKind: plugin.kind, + registry: runtimeReport, + }); + return { config: result.config, warnings: result.warnings }; + } + if (!plugin.kind) { + const runtimeReport = buildPluginDiagnosticsReport({ config }); + const runtimePlugin = runtimeReport.plugins.find((entry) => entry.id === plugin.id); + if (runtimePlugin?.kind) { + const result = applyExclusiveSlotSelection({ + config, + selectedId: runtimePlugin.id, + selectedKind: runtimePlugin.kind, + registry: runtimeReport, + }); + return { config: result.config, warnings: result.warnings }; + } + } const result = applyExclusiveSlotSelection({ config, selectedId: plugin.id, diff --git a/src/cli/plugins-install-persist.test.ts b/src/cli/plugins-install-persist.test.ts index 6aec2ce7cd9..232741445b1 100644 --- a/src/cli/plugins-install-persist.test.ts +++ b/src/cli/plugins-install-persist.test.ts @@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { applyExclusiveSlotSelection, + buildPluginDiagnosticsReport, + buildPluginSnapshotReport, enablePluginInConfig, refreshPluginRegistry, resetPluginsCliTestState, @@ -109,6 +111,168 @@ describe("persistPluginInstall", () => { expect(next).toEqual(enabledConfig); }); + it("falls back to runtime kind registry cleanup when metadata omits kind", async () => { + const { persistPluginInstall } = await import("./plugins-install-persist.js"); + const baseConfig = { + plugins: { + entries: { + "legacy-memory-a": { enabled: true }, + }, + }, + } as OpenClawConfig; + const enabledConfig = { + plugins: { + entries: { + "legacy-memory-a": { enabled: true }, + "legacy-memory": { enabled: true }, + }, + }, + } as OpenClawConfig; + enablePluginInConfig.mockReturnValue({ config: enabledConfig }); + buildPluginSnapshotReport.mockReturnValue({ + plugins: [{ id: "legacy-memory-a" }, { id: "legacy-memory" }], + diagnostics: [], + }); + buildPluginDiagnosticsReport.mockReturnValue({ + plugins: [ + { id: "legacy-memory-a", kind: "memory" }, + { id: "legacy-memory", kind: "memory" }, + ], + diagnostics: [], + }); + applyExclusiveSlotSelection.mockImplementation(((params: { + config: OpenClawConfig; + selectedId: string; + selectedKind?: string; + registry?: { plugins: Array<{ id: string; kind?: string }> }; + }) => { + expect(params.selectedId).toBe("legacy-memory"); + expect(params.selectedKind).toBe("memory"); + expect(params.registry?.plugins).toEqual([ + { id: "legacy-memory-a", kind: "memory" }, + { id: "legacy-memory", kind: "memory" }, + ]); + return { + config: { + ...params.config, + plugins: { + ...params.config.plugins, + entries: { + ...params.config.plugins?.entries, + "legacy-memory-a": { enabled: false }, + }, + slots: { + ...params.config.plugins?.slots, + memory: "legacy-memory", + }, + }, + }, + warnings: [], + changed: true, + }; + }) as (...args: unknown[]) => unknown); + + const next = await persistPluginInstall({ + snapshot: { + config: baseConfig, + baseHash: "config-1", + }, + pluginId: "legacy-memory", + install: { + source: "path", + sourcePath: "/tmp/legacy-memory", + installPath: "/tmp/legacy-memory", + }, + }); + + expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({ + config: enabledConfig, + }); + expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false); + expect(next.plugins?.slots?.memory).toBe("legacy-memory"); + }); + + it("uses runtime registry cleanup when a manifest-kind plugin has runtime-kind siblings", async () => { + const { persistPluginInstall } = await import("./plugins-install-persist.js"); + const baseConfig = { + plugins: { + entries: { + "legacy-memory-a": { enabled: true }, + }, + }, + } as OpenClawConfig; + const enabledConfig = { + plugins: { + entries: { + "legacy-memory-a": { enabled: true }, + "memory-b": { enabled: true }, + }, + }, + } as OpenClawConfig; + enablePluginInConfig.mockReturnValue({ config: enabledConfig }); + buildPluginSnapshotReport.mockReturnValue({ + plugins: [{ id: "legacy-memory-a" }, { id: "memory-b", kind: "memory" }], + diagnostics: [], + }); + buildPluginDiagnosticsReport.mockReturnValue({ + plugins: [ + { id: "legacy-memory-a", kind: "memory" }, + { id: "memory-b", kind: "memory" }, + ], + diagnostics: [], + }); + applyExclusiveSlotSelection.mockImplementation(((params: { + config: OpenClawConfig; + selectedId: string; + selectedKind?: string; + registry?: { plugins: Array<{ id: string; kind?: string }> }; + }) => { + expect(params.selectedId).toBe("memory-b"); + expect(params.selectedKind).toBe("memory"); + expect(params.registry?.plugins).toEqual([ + { id: "legacy-memory-a", kind: "memory" }, + { id: "memory-b", kind: "memory" }, + ]); + return { + config: { + ...params.config, + plugins: { + ...params.config.plugins, + entries: { + ...params.config.plugins?.entries, + "legacy-memory-a": { enabled: false }, + }, + slots: { + ...params.config.plugins?.slots, + memory: "memory-b", + }, + }, + }, + warnings: [], + changed: true, + }; + }) as (...args: unknown[]) => unknown); + + const next = await persistPluginInstall({ + snapshot: { + config: baseConfig, + baseHash: "config-1", + }, + pluginId: "memory-b", + install: { + source: "path", + sourcePath: "/tmp/memory-b", + installPath: "/tmp/memory-b", + }, + }); + + expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({ + config: enabledConfig, + }); + expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false); + expect(next.plugins?.slots?.memory).toBe("memory-b"); + }); + it("can persist an install record without enabling a plugin that needs config first", async () => { const { persistPluginInstall } = await import("./plugins-install-persist.js"); const baseConfig = { diff --git a/src/plugin-sdk/plugin-entry.ts b/src/plugin-sdk/plugin-entry.ts index d551f57434e..4a9ce81f519 100644 --- a/src/plugin-sdk/plugin-entry.ts +++ b/src/plugin-sdk/plugin-entry.ts @@ -224,6 +224,11 @@ type DefinePluginEntryOptions = { id: string; name: string; description: string; + /** + * @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via + * manifest `kind`. Runtime-entry `kind` remains only as a compatibility + * fallback for older plugins. + */ kind?: OpenClawPluginDefinition["kind"]; configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema); reload?: OpenClawPluginDefinition["reload"]; diff --git a/src/plugin-sdk/provider-entry.ts b/src/plugin-sdk/provider-entry.ts index b8339cbc1ab..f4d74c1c224 100644 --- a/src/plugin-sdk/provider-entry.ts +++ b/src/plugin-sdk/provider-entry.ts @@ -46,6 +46,11 @@ export type SingleProviderPluginOptions = { id: string; name: string; description: string; + /** + * @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via + * manifest `kind`. Runtime-entry `kind` remains only as a compatibility + * fallback for older plugins. + */ kind?: OpenClawPluginDefinition["kind"]; configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema); provider?: { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 4ce09af5416..d5be5ac7261 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -2063,6 +2063,12 @@ export type OpenClawPluginDefinition = { name?: string; description?: string; version?: string; + /** + * @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via + * manifest `kind`. Runtime-exported `kind` is kept as a compatibility + * fallback for older plugins and may require loading plugin runtime on + * metadata-only command paths. + */ kind?: PluginKind | PluginKind[]; configSchema?: OpenClawPluginConfigSchema; reload?: OpenClawPluginReloadRegistration;