Skip to content

Commit c0ec58f

Browse files
committed
fix: preserve runtime kind install fallback
1 parent a48ffda commit c0ec58f

8 files changed

Lines changed: 212 additions & 5 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
9a688c953f0108f85f58c173e79c28363d846a592130abec04cafbcabbb22dcc plugin-sdk-api-baseline.json
2-
010252e56202abde0816787588239c41b4bfb710b930a5454848a5ae76ad6dae plugin-sdk-api-baseline.jsonl
1+
a55e260675c0f02be5fe0444138683f6a0cb96d6bc6f0fa2b7026df2ea8165b2 plugin-sdk-api-baseline.json
2+
d4e9dea5aaa8a63d0f609acab2ac2962ee57ffa8bc6e5dd313a00f32cfd54b40 plugin-sdk-api-baseline.jsonl

docs/plugins/manifest.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ See [Configuration reference](/gateway/configuration) for the full `plugins.*` s
11361136
- `channels`, `providers`, `cliBackends`, and `skills` can all be omitted when a plugin does not need them.
11371137
- `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.
11381138
- Exclusive plugin kinds are selected through `plugins.slots.*`: `kind: "memory"` via `plugins.slots.memory`, `kind: "context-engine"` via `plugins.slots.contextEngine` (default `legacy`).
1139+
- Declare exclusive plugin kind in this manifest. Runtime-entry `OpenClawPluginDefinition.kind` is deprecated and remains only as a compatibility fallback for older plugins.
11391140
- 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.
11401141
- For runtime wizard metadata that requires provider code, see [Provider runtime hooks](/plugins/architecture-internals#provider-runtime-hooks).
11411142
- 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 <package>`).

src/cli/plugins-cli.install.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
66
import type { OpenClawConfig } from "../config/config.js";
77
import {
88
applyExclusiveSlotSelection,
9-
buildPluginDiagnosticsReport,
109
buildPluginSnapshotReport,
1110
clearPluginManifestRegistryCache,
1211
enablePluginInConfig,

src/cli/plugins-command-helpers.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
33
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
4-
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
5-
import { buildPluginSnapshotReport } from "../plugins/status.js";
4+
import { applyExclusiveSlotSelection, slotKeysForPluginKind } from "../plugins/slots.js";
5+
import { buildPluginDiagnosticsReport, buildPluginSnapshotReport } from "../plugins/status.js";
66
import { defaultRuntime } from "../runtime.js";
77
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
88
import { theme } from "../terminal/theme.js";
@@ -44,6 +44,33 @@ export function applySlotSelectionForPlugin(
4444
if (!plugin) {
4545
return { config, warnings: [] };
4646
}
47+
if (
48+
plugin.kind &&
49+
slotKeysForPluginKind(plugin.kind).length > 0 &&
50+
report.plugins.some((entry) => entry.id !== plugin.id && !entry.kind)
51+
) {
52+
const runtimeReport = buildPluginDiagnosticsReport({ config });
53+
const result = applyExclusiveSlotSelection({
54+
config,
55+
selectedId: plugin.id,
56+
selectedKind: plugin.kind,
57+
registry: runtimeReport,
58+
});
59+
return { config: result.config, warnings: result.warnings };
60+
}
61+
if (!plugin.kind) {
62+
const runtimeReport = buildPluginDiagnosticsReport({ config });
63+
const runtimePlugin = runtimeReport.plugins.find((entry) => entry.id === plugin.id);
64+
if (runtimePlugin?.kind) {
65+
const result = applyExclusiveSlotSelection({
66+
config,
67+
selectedId: runtimePlugin.id,
68+
selectedKind: runtimePlugin.kind,
69+
registry: runtimeReport,
70+
});
71+
return { config: result.config, warnings: result.warnings };
72+
}
73+
}
4774
const result = applyExclusiveSlotSelection({
4875
config,
4976
selectedId: plugin.id,

src/cli/plugins-install-persist.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it } from "vitest";
22
import type { OpenClawConfig } from "../config/config.js";
33
import {
44
applyExclusiveSlotSelection,
5+
buildPluginDiagnosticsReport,
6+
buildPluginSnapshotReport,
57
enablePluginInConfig,
68
refreshPluginRegistry,
79
resetPluginsCliTestState,
@@ -109,6 +111,168 @@ describe("persistPluginInstall", () => {
109111
expect(next).toEqual(enabledConfig);
110112
});
111113

114+
it("falls back to runtime kind registry cleanup when metadata omits kind", async () => {
115+
const { persistPluginInstall } = await import("./plugins-install-persist.js");
116+
const baseConfig = {
117+
plugins: {
118+
entries: {
119+
"legacy-memory-a": { enabled: true },
120+
},
121+
},
122+
} as OpenClawConfig;
123+
const enabledConfig = {
124+
plugins: {
125+
entries: {
126+
"legacy-memory-a": { enabled: true },
127+
"legacy-memory": { enabled: true },
128+
},
129+
},
130+
} as OpenClawConfig;
131+
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
132+
buildPluginSnapshotReport.mockReturnValue({
133+
plugins: [{ id: "legacy-memory-a" }, { id: "legacy-memory" }],
134+
diagnostics: [],
135+
});
136+
buildPluginDiagnosticsReport.mockReturnValue({
137+
plugins: [
138+
{ id: "legacy-memory-a", kind: "memory" },
139+
{ id: "legacy-memory", kind: "memory" },
140+
],
141+
diagnostics: [],
142+
});
143+
applyExclusiveSlotSelection.mockImplementation(((params: {
144+
config: OpenClawConfig;
145+
selectedId: string;
146+
selectedKind?: string;
147+
registry?: { plugins: Array<{ id: string; kind?: string }> };
148+
}) => {
149+
expect(params.selectedId).toBe("legacy-memory");
150+
expect(params.selectedKind).toBe("memory");
151+
expect(params.registry?.plugins).toEqual([
152+
{ id: "legacy-memory-a", kind: "memory" },
153+
{ id: "legacy-memory", kind: "memory" },
154+
]);
155+
return {
156+
config: {
157+
...params.config,
158+
plugins: {
159+
...params.config.plugins,
160+
entries: {
161+
...params.config.plugins?.entries,
162+
"legacy-memory-a": { enabled: false },
163+
},
164+
slots: {
165+
...params.config.plugins?.slots,
166+
memory: "legacy-memory",
167+
},
168+
},
169+
},
170+
warnings: [],
171+
changed: true,
172+
};
173+
}) as (...args: unknown[]) => unknown);
174+
175+
const next = await persistPluginInstall({
176+
snapshot: {
177+
config: baseConfig,
178+
baseHash: "config-1",
179+
},
180+
pluginId: "legacy-memory",
181+
install: {
182+
source: "path",
183+
sourcePath: "/tmp/legacy-memory",
184+
installPath: "/tmp/legacy-memory",
185+
},
186+
});
187+
188+
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({
189+
config: enabledConfig,
190+
});
191+
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false);
192+
expect(next.plugins?.slots?.memory).toBe("legacy-memory");
193+
});
194+
195+
it("uses runtime registry cleanup when a manifest-kind plugin has runtime-kind siblings", async () => {
196+
const { persistPluginInstall } = await import("./plugins-install-persist.js");
197+
const baseConfig = {
198+
plugins: {
199+
entries: {
200+
"legacy-memory-a": { enabled: true },
201+
},
202+
},
203+
} as OpenClawConfig;
204+
const enabledConfig = {
205+
plugins: {
206+
entries: {
207+
"legacy-memory-a": { enabled: true },
208+
"memory-b": { enabled: true },
209+
},
210+
},
211+
} as OpenClawConfig;
212+
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
213+
buildPluginSnapshotReport.mockReturnValue({
214+
plugins: [{ id: "legacy-memory-a" }, { id: "memory-b", kind: "memory" }],
215+
diagnostics: [],
216+
});
217+
buildPluginDiagnosticsReport.mockReturnValue({
218+
plugins: [
219+
{ id: "legacy-memory-a", kind: "memory" },
220+
{ id: "memory-b", kind: "memory" },
221+
],
222+
diagnostics: [],
223+
});
224+
applyExclusiveSlotSelection.mockImplementation(((params: {
225+
config: OpenClawConfig;
226+
selectedId: string;
227+
selectedKind?: string;
228+
registry?: { plugins: Array<{ id: string; kind?: string }> };
229+
}) => {
230+
expect(params.selectedId).toBe("memory-b");
231+
expect(params.selectedKind).toBe("memory");
232+
expect(params.registry?.plugins).toEqual([
233+
{ id: "legacy-memory-a", kind: "memory" },
234+
{ id: "memory-b", kind: "memory" },
235+
]);
236+
return {
237+
config: {
238+
...params.config,
239+
plugins: {
240+
...params.config.plugins,
241+
entries: {
242+
...params.config.plugins?.entries,
243+
"legacy-memory-a": { enabled: false },
244+
},
245+
slots: {
246+
...params.config.plugins?.slots,
247+
memory: "memory-b",
248+
},
249+
},
250+
},
251+
warnings: [],
252+
changed: true,
253+
};
254+
}) as (...args: unknown[]) => unknown);
255+
256+
const next = await persistPluginInstall({
257+
snapshot: {
258+
config: baseConfig,
259+
baseHash: "config-1",
260+
},
261+
pluginId: "memory-b",
262+
install: {
263+
source: "path",
264+
sourcePath: "/tmp/memory-b",
265+
installPath: "/tmp/memory-b",
266+
},
267+
});
268+
269+
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({
270+
config: enabledConfig,
271+
});
272+
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false);
273+
expect(next.plugins?.slots?.memory).toBe("memory-b");
274+
});
275+
112276
it("can persist an install record without enabling a plugin that needs config first", async () => {
113277
const { persistPluginInstall } = await import("./plugins-install-persist.js");
114278
const baseConfig = {

src/plugin-sdk/plugin-entry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ type DefinePluginEntryOptions = {
224224
id: string;
225225
name: string;
226226
description: string;
227+
/**
228+
* @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via
229+
* manifest `kind`. Runtime-entry `kind` remains only as a compatibility
230+
* fallback for older plugins.
231+
*/
227232
kind?: OpenClawPluginDefinition["kind"];
228233
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
229234
reload?: OpenClawPluginDefinition["reload"];

src/plugin-sdk/provider-entry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export type SingleProviderPluginOptions = {
4646
id: string;
4747
name: string;
4848
description: string;
49+
/**
50+
* @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via
51+
* manifest `kind`. Runtime-entry `kind` remains only as a compatibility
52+
* fallback for older plugins.
53+
*/
4954
kind?: OpenClawPluginDefinition["kind"];
5055
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
5156
provider?: {

src/plugins/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,12 @@ export type OpenClawPluginDefinition = {
20632063
name?: string;
20642064
description?: string;
20652065
version?: string;
2066+
/**
2067+
* @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via
2068+
* manifest `kind`. Runtime-exported `kind` is kept as a compatibility
2069+
* fallback for older plugins and may require loading plugin runtime on
2070+
* metadata-only command paths.
2071+
*/
20662072
kind?: PluginKind | PluginKind[];
20672073
configSchema?: OpenClawPluginConfigSchema;
20682074
reload?: OpenClawPluginReloadRegistration;

0 commit comments

Comments
 (0)