mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(doctor): repair allow-only official plugins
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Model switching: include the exact additive allowlist repair command when `/model ... --runtime ...` targets a blocked model, and make Telegram's model picker say that it changes only the session model while leaving the runtime unchanged. Thanks @vincentkoc.
|
||||
- Mattermost: clarify that the model picker only changes the session model and that runtime switches require `/oc_model <provider/model> --runtime <runtime>`. Thanks @vincentkoc.
|
||||
- Doctor/config: keep active `auth.profiles` metadata intact when `doctor --fix` strips stale secret fields from configs, repairing legacy `<provider>:default` API-key profile metadata when model fallbacks or explicit `model@profile` refs still depend on it. Fixes #77400.
|
||||
- Doctor/plugins: include `plugins.allow`-only official plugin ids in the release configured-plugin repair set, so `doctor --fix` installs official external plugins that are configured but not yet loaded instead of removing them as stale allow entries. Fixes #77155. Thanks @hclsys.
|
||||
- CLI/update: disable and skip plugins that fail package-update plugin sync, so a broken npm/ClawHub/git/marketplace plugin cannot turn a successful OpenClaw package update into a failed update result. Thanks @vincentkoc.
|
||||
- CLI/update: use an absolute POSIX npm script shell during package-manager updates, so restricted PATH environments can still run dependency lifecycle scripts while updating from `--tag main`. Fixes #77530. Thanks @PeterTremonti.
|
||||
- Diagnostics: grant the internal diagnostics event bus to official installed diagnostics exporter plugins, so npm-installed `@openclaw/diagnostics-prometheus` can emit metrics without broadening the capability to arbitrary global plugins. Fixes #76628. Thanks @RayWoo.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
detectPluginAutoEnableCandidates: vi.fn(),
|
||||
getOfficialExternalPluginCatalogEntry: vi.fn(),
|
||||
repairMissingPluginInstallsForIds: vi.fn(),
|
||||
resolveProviderInstallCatalogEntries: vi.fn(),
|
||||
}));
|
||||
@@ -14,6 +15,14 @@ vi.mock("../../../plugins/provider-install-catalog.js", () => ({
|
||||
resolveProviderInstallCatalogEntries: mocks.resolveProviderInstallCatalogEntries,
|
||||
}));
|
||||
|
||||
vi.mock(import("../../../plugins/official-external-plugin-catalog.js"), async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
getOfficialExternalPluginCatalogEntry: mocks.getOfficialExternalPluginCatalogEntry,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./missing-configured-plugin-install.js", () => ({
|
||||
repairMissingPluginInstallsForIds: mocks.repairMissingPluginInstallsForIds,
|
||||
}));
|
||||
@@ -22,6 +31,7 @@ describe("configured plugin install release step", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.detectPluginAutoEnableCandidates.mockReturnValue([]);
|
||||
mocks.getOfficialExternalPluginCatalogEntry.mockReturnValue(undefined);
|
||||
mocks.resolveProviderInstallCatalogEntries.mockReturnValue([]);
|
||||
mocks.repairMissingPluginInstallsForIds.mockResolvedValue({
|
||||
changes: [],
|
||||
@@ -372,4 +382,53 @@ describe("configured plugin install release step", () => {
|
||||
touchedConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("includes allow-only official plugin ids in the repair set", async () => {
|
||||
mocks.getOfficialExternalPluginCatalogEntry.mockImplementation((pluginId: string) => {
|
||||
if (pluginId === "lobster") {
|
||||
return { name: "@openclaw/lobster" };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { collectReleaseConfiguredPluginIds } =
|
||||
await import("./release-configured-plugin-installs.js");
|
||||
const result = collectReleaseConfiguredPluginIds({
|
||||
cfg: {
|
||||
plugins: {
|
||||
allow: ["lobster", "unofficial-custom"],
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(result.pluginIds).toEqual(["lobster"]);
|
||||
expect(result.channelIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("skips allow-only plugin ids that already have material plugin entries", async () => {
|
||||
mocks.getOfficialExternalPluginCatalogEntry.mockImplementation((pluginId: string) => {
|
||||
if (pluginId === "lobster") {
|
||||
return { name: "@openclaw/lobster" };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { collectReleaseConfiguredPluginIds } =
|
||||
await import("./release-configured-plugin-installs.js");
|
||||
const result = collectReleaseConfiguredPluginIds({
|
||||
cfg: {
|
||||
plugins: {
|
||||
allow: ["lobster"],
|
||||
entries: {
|
||||
lobster: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(result.pluginIds).toEqual(["lobster"]);
|
||||
expect(mocks.getOfficialExternalPluginCatalogEntry).not.toHaveBeenCalledWith("lobster");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { detectPluginAutoEnableCandidates } from "../../../config/plugin-auto-en
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { compareOpenClawVersions } from "../../../config/version.js";
|
||||
import { isTruthyEnvValue } from "../../../infra/env.js";
|
||||
import { getOfficialExternalPluginCatalogEntry } from "../../../plugins/official-external-plugin-catalog.js";
|
||||
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
|
||||
import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js";
|
||||
import { VERSION } from "../../../version.js";
|
||||
@@ -214,6 +215,27 @@ function collectAcpRuntimePluginIds(cfg: OpenClawConfig): string[] {
|
||||
return ["acpx"];
|
||||
}
|
||||
|
||||
function collectAllowOnlyOfficialPluginIds(cfg: OpenClawConfig): string[] {
|
||||
const allow = cfg.plugins?.allow;
|
||||
if (!Array.isArray(allow) || allow.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const materialEntryIds = new Set(
|
||||
collectMaterialPluginEntryIds(cfg).map((id) => id.toLowerCase()),
|
||||
);
|
||||
const ids: string[] = [];
|
||||
for (const rawPluginId of allow) {
|
||||
const pluginId = normalizeId(rawPluginId);
|
||||
if (!pluginId || materialEntryIds.has(pluginId.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
if (getOfficialExternalPluginCatalogEntry(pluginId)) {
|
||||
ids.push(pluginId);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function addEligiblePluginId(cfg: OpenClawConfig, pluginIds: Set<string>, pluginId: string): void {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized || isDenied(cfg, normalized) || isDisabled(cfg, normalized)) {
|
||||
@@ -274,6 +296,9 @@ export function collectReleaseConfiguredPluginIds(params: {
|
||||
for (const pluginId of collectAcpRuntimePluginIds(params.cfg)) {
|
||||
addEligiblePluginId(params.cfg, pluginIds, pluginId);
|
||||
}
|
||||
for (const pluginId of collectAllowOnlyOfficialPluginIds(params.cfg)) {
|
||||
addEligiblePluginId(params.cfg, pluginIds, pluginId);
|
||||
}
|
||||
for (const channelId of collectConfiguredChannelIds(params.cfg, env)) {
|
||||
if (
|
||||
!isChannelDisabled(params.cfg, channelId) &&
|
||||
|
||||
Reference in New Issue
Block a user