diff --git a/CHANGELOG.md b/CHANGELOG.md index abae5ac0d67..df11094f654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/externalization: repair missing configured plugin installs from npm by default, reserve ClawHub downloads for explicit `clawhubSpec` metadata, and cover agent-runtime/env-selected plugin repair. Thanks @vincentkoc. - Plugins/externalization: keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core npm package file list. - Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc. - Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc. diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts index 7e982c6218d..dad5e66d82a 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts @@ -115,7 +115,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { }); }); - it("installs a missing configured OpenClaw channel plugin from ClawHub", async () => { + it("installs a missing configured OpenClaw channel plugin from npm by default", async () => { mocks.listChannelPluginCatalogEntries.mockReturnValue([ { id: "matrix", @@ -133,33 +133,33 @@ describe("repairMissingConfiguredPluginInstalls", () => { const result = await repairMissingConfiguredPluginInstalls({ cfg: { channels: { - matrix: { enabled: true }, + matrix: { enabled: true, homeserver: "https://matrix.example.org" }, }, }, env: {}, }); - expect(mocks.installPluginFromClawHub).toHaveBeenCalledWith( + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "clawhub:@openclaw/plugin-matrix@1.2.3", + spec: "@openclaw/plugin-matrix@1.2.3", extensionsDir: "/tmp/openclaw-plugins", expectedPluginId: "matrix", + expectedIntegrity: "sha512-test", }), ); - expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled(); expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith( expect.objectContaining({ matrix: expect.objectContaining({ - source: "clawhub", - spec: "clawhub:@openclaw/plugin-matrix@1.2.3", - clawhubPackage: "@openclaw/plugin-matrix", + source: "npm", + spec: "@openclaw/plugin-matrix@1.2.3", installPath: "/tmp/openclaw-plugins/matrix", }), }), { env: {} }, ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "matrix" from clawhub:@openclaw/plugin-matrix@1.2.3.', + 'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.', ]); expect(result.warnings).toEqual([]); }); @@ -183,7 +183,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { const result = await repairMissingConfiguredPluginInstalls({ cfg: { channels: { - matrix: { enabled: true }, + matrix: { enabled: true, homeserver: "https://matrix.example.org" }, }, }, env: {}, @@ -202,6 +202,62 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(result.warnings).toEqual([]); }); + it("installs a missing channel plugin selected by environment config from npm", async () => { + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "matrix", + targetDir: "/tmp/openclaw-plugins/matrix", + version: "1.2.3", + npmResolution: { + name: "@openclaw/plugin-matrix", + version: "1.2.3", + resolvedSpec: "@openclaw/plugin-matrix@1.2.3", + integrity: "sha512-matrix", + resolvedAt: "2026-05-01T00:00:00.000Z", + }, + }); + mocks.listChannelPluginCatalogEntries.mockReturnValue([ + { + id: "matrix", + pluginId: "matrix", + meta: { label: "Matrix" }, + install: { + npmSpec: "@openclaw/plugin-matrix@1.2.3", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: {}, + env: { MATRIX_HOMESERVER: "https://matrix.example.org" }, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( + expect.objectContaining({ + spec: "@openclaw/plugin-matrix@1.2.3", + extensionsDir: "/tmp/openclaw-plugins", + expectedPluginId: "matrix", + }), + ); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith( + expect.objectContaining({ + matrix: expect.objectContaining({ + source: "npm", + spec: "@openclaw/plugin-matrix@1.2.3", + installPath: "/tmp/openclaw-plugins/matrix", + }), + }), + { env: { MATRIX_HOMESERVER: "https://matrix.example.org" } }, + ); + expect(result.changes).toEqual([ + 'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.', + ]); + expect(result.warnings).toEqual([]); + }); + it("falls back to npm when an OpenClaw channel plugin is not on ClawHub", async () => { mocks.installPluginFromClawHub.mockResolvedValueOnce({ ok: false, @@ -214,6 +270,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { pluginId: "matrix", meta: { label: "Matrix" }, install: { + clawhubSpec: "clawhub:@openclaw/plugin-matrix@stable", npmSpec: "@openclaw/plugin-matrix@1.2.3", }, }, @@ -235,7 +292,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { }), ); expect(result.changes).toEqual([ - 'ClawHub clawhub:@openclaw/plugin-matrix@1.2.3 unavailable for "matrix"; falling back to npm @openclaw/plugin-matrix@1.2.3.', + 'ClawHub clawhub:@openclaw/plugin-matrix@stable unavailable for "matrix"; falling back to npm @openclaw/plugin-matrix@1.2.3.', 'Installed missing configured plugin "matrix" from @openclaw/plugin-matrix@1.2.3.', ]); expect(result.warnings).toEqual([]); @@ -339,6 +396,126 @@ describe("repairMissingConfiguredPluginInstalls", () => { ]); }); + it("does not install disabled configured plugin entries", async () => { + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "diagnostics-otel", + label: "Diagnostics OpenTelemetry", + install: { + npmSpec: "@openclaw/diagnostics-otel", + defaultChoice: "npm", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + plugins: { + entries: { + "diagnostics-otel": { enabled: false }, + }, + }, + }, + env: {}, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled(); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled(); + expect(result).toEqual({ changes: [], warnings: [] }); + }); + + it.each([ + ["enabled-only disabled stub", { channels: { matrix: { enabled: false } } }], + [ + "disabled configured channel", + { channels: { matrix: { enabled: false, homeserver: "https://matrix.example.org" } } }, + ], + ])("does not install channel plugins for a %s", async (_label, cfg) => { + mocks.listChannelPluginCatalogEntries.mockReturnValue([ + { + id: "matrix", + pluginId: "matrix", + meta: { label: "Matrix" }, + install: { + npmSpec: "@openclaw/plugin-matrix@1.2.3", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg, + env: {}, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled(); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled(); + expect(result).toEqual({ changes: [], warnings: [] }); + }); + + it("does not install configured plugins when plugins are globally disabled", async () => { + mocks.listChannelPluginCatalogEntries.mockReturnValue([ + { + id: "matrix", + pluginId: "matrix", + meta: { label: "Matrix" }, + install: { + npmSpec: "@openclaw/plugin-matrix@1.2.3", + }, + }, + ]); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "codex", + label: "Codex", + install: { + npmSpec: "@openclaw/codex", + defaultChoice: "npm", + }, + }, + { + id: "diagnostics-otel", + label: "Diagnostics OpenTelemetry", + install: { + npmSpec: "@openclaw/diagnostics-otel", + defaultChoice: "npm", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + plugins: { + enabled: false, + entries: { + "diagnostics-otel": { enabled: true }, + }, + }, + channels: { + matrix: { homeserver: "https://matrix.example.org" }, + }, + agents: { + defaults: { + agentRuntime: { id: "codex" }, + }, + }, + }, + env: {}, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled(); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled(); + expect(result).toEqual({ changes: [], warnings: [] }); + }); + it("installs a missing third-party downloadable plugin from npm only", async () => { mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ ok: true, @@ -385,20 +562,30 @@ describe("repairMissingConfiguredPluginInstalls", () => { ]); }); - it("installs the missing configured Codex runtime plugin from the beta npm tag", async () => { + it("installs a missing default Codex runtime plugin from the official external catalog", async () => { mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ ok: true, pluginId: "codex", targetDir: "/tmp/openclaw-plugins/codex", - version: "2026.5.2-beta.1", + version: "2026.5.2", npmResolution: { name: "@openclaw/codex", - version: "2026.5.2-beta.1", - resolvedSpec: "@openclaw/codex@2026.5.2-beta.1", - integrity: "sha512-codex-beta", + version: "2026.5.2", + resolvedSpec: "@openclaw/codex@2026.5.2", + integrity: "sha512-codex", resolvedAt: "2026-05-01T00:00:00.000Z", }, }); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "codex", + label: "Codex", + install: { + npmSpec: "@openclaw/codex", + defaultChoice: "npm", + }, + }, + ]); const { repairMissingPluginInstallsForIds } = await import("./missing-configured-plugin-install.js"); @@ -418,7 +605,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(mocks.resolveProviderInstallCatalogEntries).toHaveBeenCalled(); expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( expect.objectContaining({ - spec: "@openclaw/codex@beta", + spec: "@openclaw/codex", expectedPluginId: "codex", }), ); @@ -426,19 +613,96 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect.objectContaining({ codex: expect.objectContaining({ source: "npm", - spec: "@openclaw/codex@beta", + spec: "@openclaw/codex", installPath: "/tmp/openclaw-plugins/codex", - version: "2026.5.2-beta.1", + version: "2026.5.2", }), }), { env: {} }, ); expect(result.changes).toEqual([ - 'Installed missing configured plugin "codex" from @openclaw/codex@beta.', + 'Installed missing configured plugin "codex" from @openclaw/codex.', ]); expect(result.warnings).toEqual([]); }); + it.each([ + [ + "default agent runtime", + { + agents: { + defaults: { + agentRuntime: { id: "codex" }, + }, + }, + }, + {}, + ], + [ + "agent runtime override", + { + agents: { + list: [{ id: "main", agentRuntime: { id: "codex" } }], + }, + }, + {}, + ], + ["environment runtime override", {}, { OPENCLAW_AGENT_RUNTIME: "codex" }], + ])("repairs a missing Codex plugin selected by %s", async (_label, cfg, env) => { + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "codex", + targetDir: "/tmp/openclaw-plugins/codex", + version: "2026.5.2", + npmResolution: { + name: "@openclaw/codex", + version: "2026.5.2", + resolvedSpec: "@openclaw/codex@2026.5.2", + integrity: "sha512-codex", + resolvedAt: "2026-05-01T00:00:00.000Z", + }, + }); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "codex", + label: "Codex", + install: { + npmSpec: "@openclaw/codex", + defaultChoice: "npm", + }, + }, + ]); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg, + env, + }); + + expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith( + expect.objectContaining({ + spec: "@openclaw/codex", + expectedPluginId: "codex", + }), + ); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith( + expect.objectContaining({ + codex: expect.objectContaining({ + source: "npm", + spec: "@openclaw/codex", + installPath: "/tmp/openclaw-plugins/codex", + version: "2026.5.2", + }), + }), + { env }, + ); + expect(result).toEqual({ + changes: ['Installed missing configured plugin "codex" from @openclaw/codex.'], + warnings: [], + }); + }); + it("does not install a blocked downloadable plugin from explicit channel ids", async () => { mocks.listChannelPluginCatalogEntries.mockReturnValue([ { @@ -691,4 +955,68 @@ describe("repairMissingConfiguredPluginInstalls", () => { 'Installed missing configured plugin "brave" from @openclaw/brave-plugin.', ]); }); + + it("does not install a configured external web search plugin when search is disabled", async () => { + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "brave", + label: "Brave", + install: { + npmSpec: "@openclaw/brave-plugin", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "brave", label: "Brave" }, + webSearchProviders: [ + { + id: "brave", + label: "Brave Search", + hint: "Brave Search", + envVars: ["BRAVE_API_KEY"], + placeholder: "BSA...", + signupUrl: "https://example.test/brave", + credentialPath: "plugins.entries.brave.config.webSearch.apiKey", + }, + ], + install: { + npmSpec: "@openclaw/brave-plugin", + defaultChoice: "npm", + }, + }, + }, + ]); + mocks.resolveOfficialExternalPluginId.mockImplementation( + (entry: { id?: string; openclaw?: { plugin?: { id?: string } } }) => + entry.openclaw?.plugin?.id ?? entry.id, + ); + mocks.resolveOfficialExternalPluginInstall.mockImplementation( + (entry: { install?: unknown; openclaw?: { install?: unknown } }) => + entry.openclaw?.install ?? entry.install ?? null, + ); + mocks.resolveOfficialExternalPluginLabel.mockImplementation( + (entry: { label?: string; openclaw?: { plugin?: { label?: string } } }) => + entry.openclaw?.plugin?.label ?? entry.label ?? "plugin", + ); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + tools: { + web: { + search: { + enabled: false, + provider: "brave", + }, + }, + }, + }, + env: {}, + }); + + expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); + expect(mocks.installPluginFromNpmSpec).not.toHaveBeenCalled(); + expect(mocks.writePersistedInstalledPluginIndexInstallRecords).not.toHaveBeenCalled(); + expect(result).toEqual({ changes: [], warnings: [] }); + }); }); diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 837a10c2ace..d18e26a3626 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -1,7 +1,10 @@ +import { + listExplicitlyDisabledChannelIdsForConfig, + listPotentialConfiguredChannelIds, +} from "../../../channels/config-presence.js"; import { listChannelPluginCatalogEntries } from "../../../channels/plugins/catalog.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { PluginInstallRecord } from "../../../config/types.plugins.js"; -import { parseRegistryNpmSpec } from "../../../infra/npm-registry-spec.js"; import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js"; import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js"; import { resolveDefaultPluginExtensionsDir } from "../../../plugins/install-paths.js"; @@ -41,18 +44,10 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[] { pluginId: "codex", label: "Codex", - npmSpec: "@openclaw/codex@beta", + npmSpec: "@openclaw/codex", }, ]; -function buildOpenClawClawHubSpec(npmSpec: string): string | undefined { - const parsed = parseRegistryNpmSpec(npmSpec); - if (!parsed?.name.startsWith("@openclaw/")) { - return undefined; - } - return `clawhub:${parsed.name}${parsed.selector ? `@${parsed.selector}` : ""}`; -} - function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boolean { return ( result.code === CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND || @@ -60,41 +55,58 @@ function shouldFallbackClawHubToNpm(result: { ok: false; code?: string }): boole ); } -function normalizeInstallDefaultChoice( - value: PluginPackageInstall["defaultChoice"] | undefined, -): PluginPackageInstall["defaultChoice"] | undefined { - return value === "clawhub" || value === "npm" || value === "local" ? value : undefined; -} - function resolveCandidateClawHubSpec(install: PluginPackageInstall): string | undefined { const explicit = install.clawhubSpec?.trim(); if (explicit) { return explicit; } - const npmSpec = install.npmSpec?.trim(); - if (!npmSpec || normalizeInstallDefaultChoice(install.defaultChoice) === "npm") { - return undefined; - } - return buildOpenClawClawHubSpec(npmSpec); + return undefined; } -function collectConfiguredPluginIds(cfg: OpenClawConfig): Set { +function addConfiguredPluginId(ids: Set, value: unknown): void { + if (typeof value !== "string") { + return; + } + const pluginId = value.trim(); + if (pluginId) { + ids.add(pluginId); + } +} + +function addConfiguredAgentRuntimePluginIds( + ids: Set, + cfg: OpenClawConfig, + env?: NodeJS.ProcessEnv, +): void { + addConfiguredPluginId(ids, env?.OPENCLAW_AGENT_RUNTIME); + const agents = asObjectRecord(cfg.agents); + const defaults = asObjectRecord(agents?.defaults); + addConfiguredPluginId(ids, asObjectRecord(defaults?.agentRuntime)?.id); + const list = Array.isArray(agents?.list) ? agents.list : []; + for (const entry of list) { + addConfiguredPluginId(ids, asObjectRecord(asObjectRecord(entry)?.agentRuntime)?.id); + } +} + +function collectConfiguredPluginIds(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): Set { const ids = new Set(); const plugins = asObjectRecord(cfg.plugins); + if (plugins?.enabled === false) { + return ids; + } const allow = Array.isArray(plugins?.allow) ? plugins.allow : []; for (const value of allow) { - if (typeof value === "string" && value.trim()) { - ids.add(value.trim()); - } + addConfiguredPluginId(ids, value); } const entries = asObjectRecord(plugins?.entries); - for (const pluginId of Object.keys(entries ?? {})) { - if (pluginId.trim()) { - ids.add(pluginId.trim()); + for (const [pluginId, entry] of Object.entries(entries ?? {})) { + if (asObjectRecord(entry)?.enabled === false) { + continue; } + addConfiguredPluginId(ids, pluginId); } const searchProvider = cfg.tools?.web?.search?.provider; - if (typeof searchProvider === "string") { + if (cfg.tools?.web?.search?.enabled !== false && typeof searchProvider === "string") { const installEntry = resolveWebSearchInstallCatalogEntry({ providerId: searchProvider }); if (installEntry?.pluginId) { ids.add(installEntry.pluginId); @@ -110,15 +122,27 @@ function collectConfiguredPluginIds(cfg: OpenClawConfig): Set { ) { ids.add("acpx"); } + addConfiguredAgentRuntimePluginIds(ids, cfg, env); return ids; } -function collectConfiguredChannelIds(cfg: OpenClawConfig): Set { +function collectConfiguredChannelIds(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): Set { const ids = new Set(); - const channels = asObjectRecord(cfg.channels); - for (const channelId of Object.keys(channels ?? {})) { - if (channelId !== "defaults" && channelId.trim()) { - ids.add(channelId.trim()); + if (asObjectRecord(cfg.plugins)?.enabled === false) { + return ids; + } + const disabled = new Set(listExplicitlyDisabledChannelIdsForConfig(cfg)); + const candidateChannelIds = listChannelPluginCatalogEntries({ + env, + excludeWorkspace: true, + }).map((entry) => entry.id); + for (const channelId of listPotentialConfiguredChannelIds(cfg, env, { + channelIds: candidateChannelIds, + includePersistedAuthState: false, + })) { + const normalized = channelId.trim(); + if (normalized && !disabled.has(normalized.toLowerCase())) { + ids.add(normalized); } } return ids; @@ -132,9 +156,10 @@ function collectDownloadableInstallCandidates(params: { configuredChannelIds?: ReadonlySet; blockedPluginIds?: ReadonlySet; }): DownloadableInstallCandidate[] { - const configuredPluginIds = params.configuredPluginIds ?? collectConfiguredPluginIds(params.cfg); + const configuredPluginIds = + params.configuredPluginIds ?? collectConfiguredPluginIds(params.cfg, params.env); const configuredChannelIds = - params.configuredChannelIds ?? collectConfiguredChannelIds(params.cfg); + params.configuredChannelIds ?? collectConfiguredChannelIds(params.cfg, params.env); const candidates = new Map(); for (const entry of listChannelPluginCatalogEntries({ @@ -341,8 +366,8 @@ export async function repairMissingConfiguredPluginInstalls(params: { return repairMissingPluginInstalls({ cfg: params.cfg, env: params.env, - pluginIds: collectConfiguredPluginIds(params.cfg), - channelIds: collectConfiguredChannelIds(params.cfg), + pluginIds: collectConfiguredPluginIds(params.cfg, params.env), + channelIds: collectConfiguredChannelIds(params.cfg, params.env), }); } @@ -451,5 +476,4 @@ export const __testing = { collectConfiguredChannelIds, collectConfiguredPluginIds, collectDownloadableInstallCandidates, - buildOpenClawClawHubSpec, };