mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(plugins): scope install slot selection
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- fix(infra): block ambient Homebrew env vars from brew resolution. (#74463) Thanks @pgondhi987.
|
||||
- Thinking/providers: resolve bundled provider thinking profiles through lightweight provider policy artifacts when startup-lazy providers are not active, so OpenAI Codex GPT-5.x keeps xhigh available in Gateway session validation. Fixes #74796. Thanks @maxschachere.
|
||||
- Security/Windows: ignore workspace `.env` system-path variables and resolve stale-process `taskkill.exe` from the validated Windows install root, preventing repository-local env files from redirecting cleanup helpers. Thanks @pgondhi987.
|
||||
- CLI/plugins: scope install and enable slot selection to the selected plugin manifest/runtime fallback, so plugin installs no longer load every plugin runtime or broad status snapshot just to update memory/context slots. Thanks @vincentkoc.
|
||||
- Plugins/TTS: keep bundled speech-provider discovery available on cold package Gateway paths and add bundled plugin matrix runtime probes for health, readiness, RPC, TTS discovery, and post-ready runtime-deps watchdog coverage. Refs #75283. Thanks @vincentkoc.
|
||||
- Google Meet/Twilio: show delegated voice call ID, DTMF, and intro-greeting state in `googlemeet doctor`, and avoid claiming DTMF was sent when no Meet PIN sequence was configured. Refs #72478. Thanks @DougButdorf.
|
||||
- Voice Call/Twilio: send notify-mode initial TwiML directly in the outbound create-call request while keeping conversation and pre-connect DTMF calls webhook-driven, so one-shot notify calls do not depend on a first-answer webhook fetch. Supersedes #72758. Thanks @tyshepps.
|
||||
|
||||
@@ -57,7 +57,7 @@ export const writePersistedInstalledPluginIndexInstallRecords: AsyncUnknownMock
|
||||
);
|
||||
},
|
||||
);
|
||||
const loadPluginManifestRegistry: UnknownMock = vi.fn();
|
||||
export const loadPluginManifestRegistry: UnknownMock = vi.fn();
|
||||
export const buildPluginSnapshotReport: UnknownMock = vi.fn();
|
||||
export const buildPluginRegistrySnapshotReport: UnknownMock = vi.fn();
|
||||
export const buildPluginInspectReport: UnknownMock = vi.fn();
|
||||
@@ -288,7 +288,10 @@ vi.mock("../plugins/status.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-registry.js", () => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: () => ({ diagnostics: [], plugins: [] }),
|
||||
loadPluginManifestRegistryForPluginRegistry: ((...args: unknown[]) =>
|
||||
invokeMock<unknown[], unknown>(loadPluginManifestRegistry, ...args)) as (
|
||||
...args: unknown[]
|
||||
) => unknown,
|
||||
inspectPluginRegistry: ((
|
||||
...args: Parameters<(typeof import("../plugins/plugin-registry.js"))["inspectPluginRegistry"]>
|
||||
) =>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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, slotKeysForPluginKind } from "../plugins/slots.js";
|
||||
import { buildPluginDiagnosticsReport, buildPluginSnapshotReport } from "../plugins/status.js";
|
||||
import type { PluginKind } from "../plugins/plugin-kind.types.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
|
||||
import { buildPluginDiagnosticsReport } from "../plugins/status.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
@@ -17,6 +19,59 @@ export const quietPluginJsonLogger: PluginLogger = {
|
||||
error: () => undefined,
|
||||
};
|
||||
|
||||
type SlotSelectionPlugin = {
|
||||
id: string;
|
||||
kind?: PluginKind | PluginKind[];
|
||||
};
|
||||
|
||||
type SlotSelectionRegistry = {
|
||||
plugins: readonly SlotSelectionPlugin[];
|
||||
};
|
||||
|
||||
function mergeRuntimeKinds(
|
||||
report: SlotSelectionRegistry,
|
||||
runtimeReport: SlotSelectionRegistry,
|
||||
): SlotSelectionRegistry {
|
||||
const runtimeKinds = new Map(
|
||||
runtimeReport.plugins
|
||||
.filter((plugin) => plugin.kind)
|
||||
.map((plugin) => [plugin.id, plugin.kind] as const),
|
||||
);
|
||||
return {
|
||||
plugins: report.plugins.map((plugin) => {
|
||||
if (plugin.kind) {
|
||||
return plugin;
|
||||
}
|
||||
const runtimeKind = runtimeKinds.get(plugin.id);
|
||||
return runtimeKind ? { ...plugin, kind: runtimeKind } : plugin;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function loadRuntimeKindReportForPlugins(config: OpenClawConfig, pluginIds: readonly string[]) {
|
||||
return buildPluginDiagnosticsReport({
|
||||
config,
|
||||
onlyPluginIds: [...pluginIds],
|
||||
});
|
||||
}
|
||||
|
||||
function buildSlotSelectionRegistry(
|
||||
config: OpenClawConfig,
|
||||
pluginId: string,
|
||||
): SlotSelectionRegistry {
|
||||
const registry = loadPluginManifestRegistryForPluginRegistry({
|
||||
config,
|
||||
includeDisabled: true,
|
||||
pluginIds: [pluginId],
|
||||
});
|
||||
return {
|
||||
plugins: registry.plugins.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
kind: plugin.kind,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveFileNpmSpecToLocalPath(
|
||||
raw: string,
|
||||
): { ok: true; path: string } | { ok: false; error: string } | null {
|
||||
@@ -47,34 +102,20 @@ export function applySlotSelectionForPlugin(
|
||||
config: OpenClawConfig,
|
||||
pluginId: string,
|
||||
): { config: OpenClawConfig; warnings: string[] } {
|
||||
const report = buildPluginSnapshotReport({ config });
|
||||
const report = buildSlotSelectionRegistry(config, pluginId);
|
||||
const plugin = report.plugins.find((entry) => entry.id === pluginId);
|
||||
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 runtimeReport = loadRuntimeKindReportForPlugins(config, [plugin.id]);
|
||||
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,
|
||||
registry: mergeRuntimeKinds(report, runtimeReport),
|
||||
});
|
||||
return { config: result.config, warnings: result.warnings };
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
applyExclusiveSlotSelection,
|
||||
buildPluginDiagnosticsReport,
|
||||
buildPluginSnapshotReport,
|
||||
enablePluginInConfig,
|
||||
loadPluginManifestRegistry,
|
||||
refreshPluginRegistry,
|
||||
resetPluginsCliTestState,
|
||||
writeConfigFile,
|
||||
@@ -111,7 +111,7 @@ describe("persistPluginInstall", () => {
|
||||
expect(next).toEqual(enabledConfig);
|
||||
});
|
||||
|
||||
it("falls back to runtime kind registry cleanup when metadata omits kind", async () => {
|
||||
it("scopes runtime kind lookup to the selected plugin when metadata omits kind", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
@@ -129,15 +129,12 @@ describe("persistPluginInstall", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [{ id: "legacy-memory-a" }, { id: "legacy-memory" }],
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [{ id: "legacy-memory" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "legacy-memory-a", kind: "memory" },
|
||||
{ id: "legacy-memory", kind: "memory" },
|
||||
],
|
||||
buildPluginDiagnosticsReport.mockReturnValueOnce({
|
||||
plugins: [{ id: "legacy-memory", kind: "memory" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
applyExclusiveSlotSelection.mockImplementation(((params: {
|
||||
@@ -148,19 +145,12 @@ describe("persistPluginInstall", () => {
|
||||
}) => {
|
||||
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" },
|
||||
]);
|
||||
expect(params.registry?.plugins).toEqual([{ 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",
|
||||
@@ -185,14 +175,21 @@ describe("persistPluginInstall", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildPluginDiagnosticsReport).toHaveBeenCalledTimes(1);
|
||||
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
onlyPluginIds: ["legacy-memory"],
|
||||
});
|
||||
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
includeDisabled: true,
|
||||
pluginIds: ["legacy-memory"],
|
||||
});
|
||||
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(true);
|
||||
expect(next.plugins?.slots?.memory).toBe("legacy-memory");
|
||||
});
|
||||
|
||||
it("uses runtime registry cleanup when a manifest-kind plugin has runtime-kind siblings", async () => {
|
||||
it("uses cold metadata for manifest-kind slot selection without loading runtime siblings", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
@@ -210,15 +207,8 @@ describe("persistPluginInstall", () => {
|
||||
},
|
||||
} 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" },
|
||||
],
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [{ id: "memory-b", kind: "memory" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
applyExclusiveSlotSelection.mockImplementation(((params: {
|
||||
@@ -229,19 +219,12 @@ describe("persistPluginInstall", () => {
|
||||
}) => {
|
||||
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" },
|
||||
]);
|
||||
expect(params.registry?.plugins).toEqual([{ 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",
|
||||
@@ -266,11 +249,69 @@ describe("persistPluginInstall", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildPluginDiagnosticsReport).not.toHaveBeenCalled();
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
includeDisabled: true,
|
||||
pluginIds: ["memory-b"],
|
||||
});
|
||||
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(true);
|
||||
expect(next.plugins?.slots?.memory).toBe("memory-b");
|
||||
});
|
||||
|
||||
it("does not load every plugin runtime for non-slot installs without manifest kind", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
plain: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [{ id: "plain" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
plugins: [{ id: "plain" }],
|
||||
diagnostics: [],
|
||||
});
|
||||
applyExclusiveSlotSelection.mockReturnValue({
|
||||
config: enabledConfig,
|
||||
warnings: [],
|
||||
changed: false,
|
||||
});
|
||||
|
||||
const next = await persistPluginInstall({
|
||||
snapshot: {
|
||||
config: baseConfig,
|
||||
baseHash: "config-1",
|
||||
},
|
||||
pluginId: "plain",
|
||||
install: {
|
||||
source: "path",
|
||||
sourcePath: "/tmp/plain",
|
||||
installPath: "/tmp/plain",
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildPluginDiagnosticsReport).toHaveBeenCalledTimes(1);
|
||||
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
onlyPluginIds: ["plain"],
|
||||
});
|
||||
expect(next.plugins?.entries?.["legacy-memory-a"]?.enabled).toBe(false);
|
||||
expect(next.plugins?.slots?.memory).toBe("memory-b");
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: enabledConfig,
|
||||
includeDisabled: true,
|
||||
pluginIds: ["plain"],
|
||||
});
|
||||
expect(next).toEqual(enabledConfig);
|
||||
});
|
||||
|
||||
it("can persist an install record without enabling a plugin that needs config first", async () => {
|
||||
|
||||
Reference in New Issue
Block a user