From bc24b547d0888fb716505b07e3f2dceeb8abd203 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 19:26:16 -0700 Subject: [PATCH] fix(agents): resolve plugin skill metadata cold --- src/agents/pi-project-settings-snapshot.ts | 48 +++++++++++++++++-- src/agents/pi-project-settings.bundle.test.ts | 8 +++- src/agents/skills/plugin-skills.test.ts | 35 ++++++++++---- src/agents/skills/plugin-skills.ts | 15 ++++-- 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/pi-project-settings-snapshot.ts index ee0e76910d8..8487d7212e8 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/pi-project-settings-snapshot.ts @@ -7,10 +7,12 @@ import { openBoundaryFileSync } from "../infra/boundary-file-read.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import type { BundleMcpServerConfig } from "../plugins/bundle-mcp.js"; import { - normalizePluginsConfig, + normalizePluginsConfigWithResolver, resolveEffectivePluginActivationState, -} from "../plugins/config-state.js"; -import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; +} from "../plugins/config-policy.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { loadPluginRegistrySnapshot } from "../plugins/plugin-registry.js"; import { isRecord } from "../utils.js"; import { loadEmbeddedPiMcpConfig } from "./embedded-pi-mcp.js"; @@ -68,6 +70,33 @@ function loadBundleSettingsFile(params: { } } +function buildRegistryPluginIdAliases( + registry: PluginManifestRegistry, +): Readonly> { + return Object.fromEntries( + registry.plugins + .flatMap((record) => [ + ...(record.providers ?? []) + .filter((providerId) => providerId !== record.id) + .map((providerId) => [providerId, record.id] as const), + ...(record.legacyPluginIds ?? []).map( + (legacyPluginId) => [legacyPluginId, record.id] as const, + ), + ]) + .toSorted(([left], [right]) => left.localeCompare(right)), + ); +} + +function createRegistryPluginIdNormalizer( + registry: PluginManifestRegistry, +): (id: string) => string { + const aliases = buildRegistryPluginIdAliases(registry); + return (id: string) => { + const trimmed = id.trim(); + return aliases[trimmed] ?? trimmed; + }; +} + export function loadEnabledBundlePiSettingsSnapshot(params: { cwd: string; cfg?: OpenClawConfig; @@ -76,15 +105,24 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { if (!workspaceDir) { return {}; } - const registry = loadPluginManifestRegistry({ + const index = loadPluginRegistrySnapshot({ workspaceDir, config: params.cfg, }); + const registry = loadPluginManifestRegistryForInstalledIndex({ + index, + workspaceDir, + config: params.cfg, + includeDisabled: true, + }); if (registry.plugins.length === 0) { return {}; } - const normalizedPlugins = normalizePluginsConfig(params.cfg?.plugins); + const normalizedPlugins = normalizePluginsConfigWithResolver( + params.cfg?.plugins, + createRegistryPluginIdNormalizer(registry), + ); let snapshot: PiSettingsSnapshot = {}; for (const record of registry.plugins) { diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/pi-project-settings.bundle.test.ts index f741e9b68e4..a2198b6405e 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/pi-project-settings.bundle.test.ts @@ -13,11 +13,11 @@ vi.mock("../infra/boundary-file-read.js", async () => { }; }); -vi.mock("../plugins/manifest-registry.js", async () => { +vi.mock("../plugins/manifest-registry-installed.js", async () => { const fs = await import("node:fs"); const path = await import("node:path"); return { - loadPluginManifestRegistry: (params: { workspaceDir?: string }) => { + loadPluginManifestRegistryForInstalledIndex: (params: { workspaceDir?: string }) => { const rootDir = path.join( params.workspaceDir ?? "", ".openclaw", @@ -45,6 +45,10 @@ vi.mock("../plugins/manifest-registry.js", async () => { }; }); +vi.mock("../plugins/plugin-registry.js", () => ({ + loadPluginRegistrySnapshot: () => ({ plugins: [] }), +})); + vi.mock("./embedded-pi-mcp.js", async () => { const fs = await import("node:fs"); const path = await import("node:path"); diff --git a/src/agents/skills/plugin-skills.test.ts b/src/agents/skills/plugin-skills.test.ts index 47d39986c55..480276b7377 100644 --- a/src/agents/skills/plugin-skills.test.ts +++ b/src/agents/skills/plugin-skills.test.ts @@ -6,11 +6,17 @@ import type { PluginManifestRegistry } from "../../plugins/manifest-registry.js" import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js"; const hoisted = vi.hoisted(() => ({ - loadPluginManifestRegistry: vi.fn(), + loadPluginManifestRegistryForInstalledIndex: vi.fn(), + loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })), })); -vi.mock("../../plugins/manifest-registry.js", () => ({ - loadPluginManifestRegistry: (...args: unknown[]) => hoisted.loadPluginManifestRegistry(...args), +vi.mock("../../plugins/manifest-registry-installed.js", () => ({ + loadPluginManifestRegistryForInstalledIndex: (...args: unknown[]) => + hoisted.loadPluginManifestRegistryForInstalledIndex(...args), +})); + +vi.mock("../../plugins/plugin-registry.js", () => ({ + loadPluginRegistrySnapshot: (...args: unknown[]) => hoisted.loadPluginRegistrySnapshot(...args), })); let resolvePluginSkillDirs: typeof import("./plugin-skills.js").resolvePluginSkillDirs; @@ -85,7 +91,9 @@ async function setupAcpxAndHelperRegistry() { const helperRoot = await tempDirs.make("openclaw-helper-plugin-"); await fs.mkdir(path.join(acpxRoot, "skills"), { recursive: true }); await fs.mkdir(path.join(helperRoot, "skills"), { recursive: true }); - hoisted.loadPluginManifestRegistry.mockReturnValue(buildRegistry({ acpxRoot, helperRoot })); + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( + buildRegistry({ acpxRoot, helperRoot }), + ); return { workspaceDir, acpxRoot, helperRoot }; } @@ -98,7 +106,8 @@ async function setupPluginOutsideSkills() { } afterEach(async () => { - hoisted.loadPluginManifestRegistry.mockReset(); + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReset(); + hoisted.loadPluginRegistrySnapshot.mockReset(); await tempDirs.cleanup(); }); @@ -108,7 +117,13 @@ describe("resolvePluginSkillDirs", () => { }); beforeEach(() => { - hoisted.loadPluginManifestRegistry.mockReset(); + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReset(); + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ + diagnostics: [], + plugins: [], + }); + hoisted.loadPluginRegistrySnapshot.mockReset(); + hoisted.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] }); }); it.each([ @@ -152,7 +167,7 @@ describe("resolvePluginSkillDirs", () => { await fs.mkdir(outsideSkills, { recursive: true }); const escapePath = path.relative(pluginRoot, outsideSkills); - hoisted.loadPluginManifestRegistry.mockReturnValue( + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createSinglePluginRegistry({ pluginRoot, skills: ["./skills", escapePath], @@ -183,7 +198,7 @@ describe("resolvePluginSkillDirs", () => { process.platform === "win32" ? ("junction" as const) : ("dir" as const), ); - hoisted.loadPluginManifestRegistry.mockReturnValue( + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createSinglePluginRegistry({ pluginRoot, skills: ["./skills-link"], @@ -210,7 +225,7 @@ describe("resolvePluginSkillDirs", () => { await fs.mkdir(path.join(pluginRoot, "commands"), { recursive: true }); await fs.mkdir(path.join(pluginRoot, "skills"), { recursive: true }); - hoisted.loadPluginManifestRegistry.mockReturnValue( + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createSinglePluginRegistry({ pluginRoot, format: "bundle", @@ -240,7 +255,7 @@ describe("resolvePluginSkillDirs", () => { const pluginRoot = await tempDirs.make("openclaw-legacy-plugin-"); await fs.mkdir(path.join(pluginRoot, "skills"), { recursive: true }); - hoisted.loadPluginManifestRegistry.mockReturnValue( + hoisted.loadPluginManifestRegistryForInstalledIndex.mockReturnValue( createSinglePluginRegistry({ pluginRoot, skills: ["./skills"], diff --git a/src/agents/skills/plugin-skills.ts b/src/agents/skills/plugin-skills.ts index dfd0a46ed53..20d71245293 100644 --- a/src/agents/skills/plugin-skills.ts +++ b/src/agents/skills/plugin-skills.ts @@ -7,10 +7,9 @@ import { resolveEffectivePluginActivationState, resolveMemorySlotDecision, } from "../../plugins/config-policy.js"; -import { - loadPluginManifestRegistry, - type PluginManifestRegistry, -} from "../../plugins/manifest-registry.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "../../plugins/manifest-registry-installed.js"; +import type { PluginManifestRegistry } from "../../plugins/manifest-registry.js"; +import { loadPluginRegistrySnapshot } from "../../plugins/plugin-registry.js"; import { hasKind } from "../../plugins/slots.js"; import { isPathInsideWithRealpath } from "../../security/scan-paths.js"; @@ -51,10 +50,16 @@ export function resolvePluginSkillDirs(params: { if (!workspaceDir) { return []; } - const registry = loadPluginManifestRegistry({ + const index = loadPluginRegistrySnapshot({ workspaceDir, config: params.config, }); + const registry = loadPluginManifestRegistryForInstalledIndex({ + index, + workspaceDir, + config: params.config, + includeDisabled: true, + }); if (registry.plugins.length === 0) { return []; }