From eef8dab4e9336042b66d6bddbcf8882d3611467c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 01:55:09 +0100 Subject: [PATCH] refactor: route bundled catalogs through plugin registry --- .../bundled-channel-catalog-read.test.ts | 37 ++++++++- src/channels/bundled-channel-catalog-read.ts | 55 ++++--------- .../zod-schema.providers.lazy-runtime.test.ts | 78 ++++++++++++------- src/config/zod-schema.providers.ts | 42 ++-------- .../bundled-package-channel-metadata.test.ts | 43 +++++++++- .../bundled-package-channel-metadata.ts | 33 +------- src/plugins/effective-plugin-ids.ts | 45 ++++++----- src/plugins/provider-public-artifacts.test.ts | 26 ++++++- src/plugins/provider-public-artifacts.ts | 33 +++----- src/secrets/target-registry-data.ts | 17 ++-- 10 files changed, 213 insertions(+), 196 deletions(-) diff --git a/src/channels/bundled-channel-catalog-read.test.ts b/src/channels/bundled-channel-catalog-read.test.ts index 40db580a6df..69f3a8e4414 100644 --- a/src/channels/bundled-channel-catalog-read.test.ts +++ b/src/channels/bundled-channel-catalog-read.test.ts @@ -27,13 +27,35 @@ import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { listBundledChannelCatalogEntries } from "./bundled-channel-catalog-read.js"; const tempDirs: string[] = []; +const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; +const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; afterEach(() => { + if (originalBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir; + } + if (originalTrustBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir; + } cleanupTempDirs(tempDirs); vi.restoreAllMocks(); vi.mocked(resolveBundledPluginsDir).mockReset(); }); +function useBundledPluginsDir(extensionsRoot: string | undefined): void { + if (extensionsRoot) { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = extensionsRoot; + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1"; + } else { + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + } + vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); +} + function seedRoot(prefix: string): string { const root = makeTempRepoRoot(tempDirs, prefix); writeJsonFile(path.join(root, "package.json"), { name: "openclaw" }); @@ -45,6 +67,7 @@ function seedChannelPkg( pkgJsonPath: string, opts: { id: string; docsPath: string; label?: string; blurb?: string }, ): void { + const pluginDir = path.dirname(pkgJsonPath); writeJsonFile(pkgJsonPath, { name: `@openclaw/${opts.id}`, openclaw: { @@ -56,6 +79,12 @@ function seedChannelPkg( }, }, }); + writeJsonFile(path.join(pluginDir, "openclaw.plugin.json"), { + id: opts.id, + configSchema: { type: "object" }, + channels: [opts.id], + }); + fs.writeFileSync(path.join(pluginDir, "index.js"), "export default { register() {} };\n", "utf8"); } describe("listBundledChannelCatalogEntries", () => { @@ -75,7 +104,7 @@ describe("listBundledChannelCatalogEntries", () => { id: "imessage", docsPath: "/channels/imessage", }); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); + useBundledPluginsDir(extensionsRoot); const entries = listBundledChannelCatalogEntries(); @@ -109,7 +138,7 @@ describe("listBundledChannelCatalogEntries", () => { }, ], }); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); + useBundledPluginsDir(extensionsRoot); const entries = listBundledChannelCatalogEntries(); expect(entries.map((entry) => entry.id)).toEqual(expect.arrayContaining(["qqbot", "telegram"])); @@ -136,7 +165,7 @@ describe("listBundledChannelCatalogEntries", () => { }, ], }); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(undefined); + useBundledPluginsDir(undefined); const entries = listBundledChannelCatalogEntries(); expect(entries.map((entry) => entry.id)).toContain("fallback-channel"); @@ -165,7 +194,7 @@ describe("listBundledChannelCatalogEntries", () => { }, ], }); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); + useBundledPluginsDir(extensionsRoot); const entries = listBundledChannelCatalogEntries(); expect(entries.map((entry) => entry.id)).toContain("fallback-channel"); diff --git a/src/channels/bundled-channel-catalog-read.ts b/src/channels/bundled-channel-catalog-read.ts index c9f0a25b689..4e4c2d571aa 100644 --- a/src/channels/bundled-channel-catalog-read.ts +++ b/src/channels/bundled-channel-catalog-read.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; -import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; +import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js"; import type { PluginPackageChannel } from "../plugins/manifest.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; @@ -27,43 +27,8 @@ function listPackageRoots(): string[] { ].filter((entry, index, all): entry is string => Boolean(entry) && all.indexOf(entry) === index); } -function listBundledExtensionPackageJsonPaths(env: NodeJS.ProcessEnv = process.env): string[] { - // Delegate to the plugin loader's resolver so channel metadata stays in lock - // step with whichever bundled plugin tree is actually loaded at runtime - // (source extensions/ in dev/test, dist/extensions in published installs, - // dist-runtime/extensions when paired with dist, etc.). See - // src/plugins/bundled-dir.ts for the full candidate-order policy and - // src/plugins/bundled-dir.test.ts for the precedence coverage. Reusing the - // resolver also picks up OPENCLAW_BUNDLED_PLUGINS_DIR overrides and the - // bun --compile sibling layout for free. - const extensionsRoot = resolveBundledPluginsDir(env); - if (!extensionsRoot) { - return []; - } - try { - return fs - .readdirSync(extensionsRoot, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => path.join(extensionsRoot, entry.name, "package.json")) - .filter((entry) => fs.existsSync(entry)); - } catch { - return []; - } -} - -function readBundledExtensionCatalogEntriesSync(): ChannelCatalogEntryLike[] { - const entries: ChannelCatalogEntryLike[] = []; - for (const packageJsonPath of listBundledExtensionPackageJsonPaths()) { - try { - const payload = JSON.parse( - fs.readFileSync(packageJsonPath, "utf8"), - ) as ChannelCatalogEntryLike; - entries.push(payload); - } catch { - continue; - } - } - return entries; +function readBundledExtensionCatalogEntriesSync(): PluginPackageChannel[] { + return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel); } function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] { @@ -84,8 +49,18 @@ function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] { return []; } -function toBundledChannelEntry(entry: ChannelCatalogEntryLike): BundledChannelCatalogEntry | null { - const channel = entry.openclaw?.channel; +function isChannelCatalogEntryLike( + entry: ChannelCatalogEntryLike | PluginPackageChannel, +): entry is ChannelCatalogEntryLike { + return "openclaw" in entry; +} + +function toBundledChannelEntry( + entry: ChannelCatalogEntryLike | PluginPackageChannel, +): BundledChannelCatalogEntry | null { + const channel: PluginPackageChannel | undefined = isChannelCatalogEntryLike(entry) + ? entry.openclaw?.channel + : entry; const id = normalizeOptionalLowercaseString(channel?.id); if (!id || !channel) { return null; diff --git a/src/config/zod-schema.providers.lazy-runtime.test.ts b/src/config/zod-schema.providers.lazy-runtime.test.ts index 1ba42b05239..fc7c84e5fbf 100644 --- a/src/config/zod-schema.providers.lazy-runtime.test.ts +++ b/src/config/zod-schema.providers.lazy-runtime.test.ts @@ -1,10 +1,13 @@ import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { BundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import type { PluginManifestChannelConfig } from "../plugins/manifest.js"; -const listBundledPluginMetadataMock = vi.hoisted(() => - vi.fn<(options?: unknown) => readonly BundledPluginMetadata[]>(() => []), +const loadPluginManifestRegistryMock = vi.hoisted(() => + vi.fn<(options?: Record) => PluginManifestRegistry>(() => ({ + plugins: [], + diagnostics: [], + })), ); const collectBundledChannelConfigsMock = vi.hoisted(() => vi.fn<(params: unknown) => Record | undefined>( @@ -14,10 +17,15 @@ const collectBundledChannelConfigsMock = vi.hoisted(() => describe("ChannelsSchema bundled runtime loading", () => { beforeEach(() => { - listBundledPluginMetadataMock.mockClear(); + loadPluginManifestRegistryMock.mockClear(); + loadPluginManifestRegistryMock.mockReturnValue({ + plugins: [], + diagnostics: [], + }); collectBundledChannelConfigsMock.mockClear(); - vi.doMock("../plugins/bundled-plugin-metadata.js", () => ({ - listBundledPluginMetadata: (options?: unknown) => listBundledPluginMetadataMock(options), + vi.doMock("../plugins/plugin-registry.js", () => ({ + loadPluginManifestRegistryForPluginRegistry: (options?: Record) => + loadPluginManifestRegistryMock(options), })); vi.doMock("../plugins/bundled-channel-config-metadata.js", () => ({ collectBundledChannelConfigs: (params: unknown) => collectBundledChannelConfigsMock(params), @@ -42,18 +50,20 @@ describe("ChannelsSchema bundled runtime loading", () => { }); expect(parsed?.defaults?.groupPolicy).toBe("open"); - expect(listBundledPluginMetadataMock).not.toHaveBeenCalledWith( + expect(loadPluginManifestRegistryMock).not.toHaveBeenCalledWith( expect.objectContaining({ - includeChannelConfigs: true, + bundledChannelConfigCollector: expect.any(Function), }), ); }); it("loads bundled channel runtime discovery only when plugin-owned channel config is present", async () => { - listBundledPluginMetadataMock.mockReturnValueOnce([ - { - dirName: "discord", - manifest: { + loadPluginManifestRegistryMock.mockReturnValueOnce({ + diagnostics: [], + plugins: [ + { + id: "discord", + origin: "bundled", channels: ["discord"], channelConfigs: { discord: { @@ -62,9 +72,9 @@ describe("ChannelsSchema bundled runtime loading", () => { }, }, }, - }, - } as unknown as BundledPluginMetadata, - ]); + } as unknown as PluginManifestRegistry["plugins"][number], + ], + }); const runtime = await importFreshModule( import.meta.url, @@ -75,24 +85,16 @@ describe("ChannelsSchema bundled runtime loading", () => { discord: {}, }); - expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([ + expect(loadPluginManifestRegistryMock.mock.calls).toContainEqual([ expect.objectContaining({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, + includeDisabled: true, + bundledChannelConfigCollector: expect.any(Function), }), ]); expect(collectBundledChannelConfigsMock).not.toHaveBeenCalled(); }); it("loads a single plugin-owned runtime surface when the manifest omits runtime metadata", async () => { - listBundledPluginMetadataMock.mockReturnValueOnce([ - { - dirName: "discord", - manifest: { - channels: ["discord"], - }, - } as unknown as BundledPluginMetadata, - ]); collectBundledChannelConfigsMock.mockReturnValueOnce({ discord: { schema: {}, @@ -101,6 +103,24 @@ describe("ChannelsSchema bundled runtime loading", () => { }, }, }); + loadPluginManifestRegistryMock.mockImplementationOnce((options) => ({ + diagnostics: [], + plugins: [ + { + id: "discord", + origin: "bundled", + channels: ["discord"], + channelConfigs: ( + options?.bundledChannelConfigCollector as + | ((params: unknown) => Record | undefined) + | undefined + )?.({ + pluginDir: "/repo/extensions/discord", + manifest: { id: "discord", channels: ["discord"] }, + }), + } as unknown as PluginManifestRegistry["plugins"][number], + ], + })); const runtime = await importFreshModule( import.meta.url, @@ -111,10 +131,10 @@ describe("ChannelsSchema bundled runtime loading", () => { discord: {}, }); - expect(listBundledPluginMetadataMock.mock.calls).toContainEqual([ + expect(loadPluginManifestRegistryMock.mock.calls).toContainEqual([ expect.objectContaining({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, + includeDisabled: true, + bundledChannelConfigCollector: expect.any(Function), }), ]); expect(collectBundledChannelConfigsMock).toHaveBeenCalledTimes(1); diff --git a/src/config/zod-schema.providers.ts b/src/config/zod-schema.providers.ts index 77f8fb9646c..3bd822675b2 100644 --- a/src/config/zod-schema.providers.ts +++ b/src/config/zod-schema.providers.ts @@ -1,10 +1,6 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; import { z } from "zod"; -import type { ChannelConfigRuntimeSchema } from "../channels/plugins/types.config.js"; import { collectBundledChannelConfigs } from "../plugins/bundled-channel-config-metadata.js"; -import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; -import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js"; +import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import type { ChannelsConfig } from "./types.channels.js"; import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js"; import { ContextVisibilityModeSchema, GroupPolicySchema } from "./zod-schema.core.js"; @@ -17,36 +13,12 @@ const ChannelModelByChannelSchema = z .record(z.string(), z.record(z.string(), z.string())) .optional(); -const OPENCLAW_PACKAGE_ROOT = - resolveLoaderPackageRoot({ - modulePath: fileURLToPath(import.meta.url), - moduleUrl: import.meta.url, - }) ?? fileURLToPath(new URL("../..", import.meta.url)); - -function getDirectChannelRuntimeSchema(channelId: string): ChannelConfigRuntimeSchema | undefined { - for (const entry of listBundledPluginMetadata({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - })) { - const manifestRuntime = entry.manifest.channelConfigs?.[channelId]?.runtime; - if (manifestRuntime) { - return manifestRuntime; - } - if (!entry.manifest.channels?.includes(channelId)) { - continue; - } - const collectedChannelConfigs = collectBundledChannelConfigs({ - pluginDir: path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", entry.dirName), - manifest: entry.manifest, - ...(entry.packageManifest ? { packageManifest: entry.packageManifest } : {}), - }); - const collectedRuntime = collectedChannelConfigs?.[channelId]?.runtime; - if (collectedRuntime) { - return collectedRuntime; - } - } - - return undefined; +function getDirectChannelRuntimeSchema(channelId: string) { + return loadPluginManifestRegistryForPluginRegistry({ + includeDisabled: true, + bundledChannelConfigCollector: collectBundledChannelConfigs, + }).plugins.find((plugin) => plugin.origin === "bundled" && plugin.channelConfigs?.[channelId]) + ?.channelConfigs?.[channelId]?.runtime; } function hasPluginOwnedChannelConfig( diff --git a/src/plugins/bundled-package-channel-metadata.test.ts b/src/plugins/bundled-package-channel-metadata.test.ts index 1fc269366cc..ace62af5c42 100644 --- a/src/plugins/bundled-package-channel-metadata.test.ts +++ b/src/plugins/bundled-package-channel-metadata.test.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js"; @@ -10,13 +11,31 @@ import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { findBundledPackageChannelMetadata } from "./bundled-package-channel-metadata.js"; const tempDirs: string[] = []; +const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; +const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; afterEach(() => { + if (originalBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir; + } + if (originalTrustBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir; + } cleanupTempDirs(tempDirs); vi.restoreAllMocks(); vi.mocked(resolveBundledPluginsDir).mockReset(); }); +function useBundledPluginsDir(extensionsRoot: string): void { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = extensionsRoot; + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1"; + vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); +} + describe("bundled package channel metadata", () => { it("reads doctor capabilities from the resolved bundled plugin dir", () => { const root = makeTempRepoRoot(tempDirs, "bpcm-"); @@ -37,7 +56,17 @@ describe("bundled package channel metadata", () => { }, }, }); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); + writeJsonFile(path.join(extensionsRoot, "matrix", "openclaw.plugin.json"), { + id: "matrix", + configSchema: { type: "object" }, + channels: ["matrix"], + }); + fs.writeFileSync( + path.join(extensionsRoot, "matrix", "index.js"), + "export default {};\n", + "utf8", + ); + useBundledPluginsDir(extensionsRoot); const matrix = findBundledPackageChannelMetadata("matrix"); @@ -53,7 +82,7 @@ describe("bundled package channel metadata", () => { const root = makeTempRepoRoot(tempDirs, "bpcm-fresh-"); const extensionsRoot = path.join(root, "dist", "extensions"); const packagePath = path.join(extensionsRoot, "matrix", "package.json"); - vi.mocked(resolveBundledPluginsDir).mockReturnValue(extensionsRoot); + useBundledPluginsDir(extensionsRoot); writeJsonFile(packagePath, { name: "@openclaw/matrix", @@ -64,6 +93,16 @@ describe("bundled package channel metadata", () => { }, }, }); + writeJsonFile(path.join(extensionsRoot, "matrix", "openclaw.plugin.json"), { + id: "matrix", + configSchema: { type: "object" }, + channels: ["matrix"], + }); + fs.writeFileSync( + path.join(extensionsRoot, "matrix", "index.js"), + "export default {};\n", + "utf8", + ); expect(findBundledPackageChannelMetadata("matrix")?.label).toBe("Before"); writeJsonFile(packagePath, { diff --git a/src/plugins/bundled-package-channel-metadata.ts b/src/plugins/bundled-package-channel-metadata.ts index fcb91532330..5decfc3e8e9 100644 --- a/src/plugins/bundled-package-channel-metadata.ts +++ b/src/plugins/bundled-package-channel-metadata.ts @@ -1,35 +1,8 @@ -import fs from "node:fs"; -import path from "node:path"; -import { resolveBundledPluginsDir } from "./bundled-dir.js"; -import { - getPackageManifestMetadata, - type PackageManifest, - type PluginPackageChannel, -} from "./manifest.js"; - -function readPackageManifest(pluginDir: string): PackageManifest | undefined { - const packagePath = path.join(pluginDir, "package.json"); - if (!fs.existsSync(packagePath)) { - return undefined; - } - try { - return JSON.parse(fs.readFileSync(packagePath, "utf-8")) as PackageManifest; - } catch { - return undefined; - } -} +import { listChannelCatalogEntries } from "./channel-catalog-registry.js"; +import type { PluginPackageChannel } from "./manifest.js"; export function listBundledPackageChannelMetadata(): readonly PluginPackageChannel[] { - const scanDir = resolveBundledPluginsDir(); - if (!scanDir || !fs.existsSync(scanDir)) { - return []; - } - return fs - .readdirSync(scanDir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => readPackageManifest(path.join(scanDir, entry.name))) - .map((manifest) => getPackageManifestMetadata(manifest)?.channel) - .filter((channel): channel is PluginPackageChannel => Boolean(channel?.id)); + return listChannelCatalogEntries({ origin: "bundled" }).map((entry) => entry.channel); } export function findBundledPackageChannelMetadata( diff --git a/src/plugins/effective-plugin-ids.ts b/src/plugins/effective-plugin-ids.ts index 3eb1c359d8b..7924203e8bc 100644 --- a/src/plugins/effective-plugin-ids.ts +++ b/src/plugins/effective-plugin-ids.ts @@ -1,5 +1,3 @@ -import fs from "node:fs"; -import path from "node:path"; import { listExplicitlyDisabledChannelIdsForConfig, listPotentialConfiguredChannelIds, @@ -7,7 +5,6 @@ import { import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { resolveBundledPluginsDir } from "./bundled-dir.js"; import { listExplicitConfiguredChannelIdsForConfig, resolveConfiguredChannelPluginIds, @@ -15,7 +12,7 @@ import { } from "./channel-plugin-ids.js"; import { normalizePluginsConfig } from "./config-state.js"; import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js"; -import { loadPluginManifest } from "./manifest.js"; +import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; function collectConfiguredChannelIds( config: OpenClawConfig, @@ -45,6 +42,7 @@ function collectBundledChannelOwnerPluginIds(params: { config: OpenClawConfig; channelIds: readonly string[]; env: NodeJS.ProcessEnv; + workspaceDir?: string; bundledPluginsDir?: string; }): string[] { const plugins = normalizePluginsConfig(params.config.plugins); @@ -56,32 +54,32 @@ function collectBundledChannelOwnerPluginIds(params: { if (channelIds.size === 0) { return []; } - const bundledDir = params.bundledPluginsDir ?? resolveBundledPluginsDir(params.env); - if (!bundledDir) { - return []; - } - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(bundledDir, { withFileTypes: true }); - } catch { - return []; - } + const env = params.bundledPluginsDir + ? { + ...params.env, + OPENCLAW_BUNDLED_PLUGINS_DIR: params.bundledPluginsDir, + ...(params.env.VITEST || process.env.VITEST + ? { OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1" } + : {}), + } + : params.env; + const registry = loadPluginManifestRegistryForPluginRegistry({ + config: params.config, + env, + workspaceDir: params.workspaceDir, + includeDisabled: true, + }); const pluginIds = new Set(); - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - const pluginDir = path.join(bundledDir, entry.name); - const manifest = loadPluginManifest(pluginDir, false); - if (!manifest.ok) { + for (const plugin of registry.plugins) { + if (plugin.origin !== "bundled") { continue; } if ( - (manifest.manifest.channels ?? []).some((channelId) => + plugin.channels.some((channelId) => channelIds.has(normalizeOptionalLowercaseString(channelId) ?? ""), ) ) { - const pluginId = normalizeOptionalLowercaseString(manifest.manifest.id); + const pluginId = normalizeOptionalLowercaseString(plugin.id); if ( pluginId && passesManifestOwnerBasePolicy({ @@ -152,6 +150,7 @@ export function resolveEffectivePluginIds(params: { config: effectiveConfig, channelIds: configuredChannelIds, env: params.env, + workspaceDir: params.workspaceDir, ...(params.bundledPluginsDir ? { bundledPluginsDir: params.bundledPluginsDir } : {}), })) { ids.add(pluginId); diff --git a/src/plugins/provider-public-artifacts.test.ts b/src/plugins/provider-public-artifacts.test.ts index 0ffb3d7085a..e54930e2a27 100644 --- a/src/plugins/provider-public-artifacts.test.ts +++ b/src/plugins/provider-public-artifacts.test.ts @@ -7,7 +7,20 @@ import type { ModelProviderConfig } from "../config/types.models.js"; import { resolveBundledProviderPolicySurface } from "./provider-public-artifacts.js"; describe("provider public artifacts", () => { + const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + const originalTrustBundledPluginsDir = process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; + afterEach(() => { + if (originalBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir; + } + if (originalTrustBundledPluginsDir === undefined) { + delete process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR; + } else { + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = originalTrustBundledPluginsDir; + } vi.doUnmock("./bundled-dir.js"); vi.doUnmock("./public-surface-loader.js"); vi.resetModules(); @@ -36,7 +49,16 @@ describe("provider public artifacts", () => { fs.mkdirSync(pluginDir, { recursive: true }); fs.writeFileSync( path.join(pluginDir, "openclaw.plugin.json"), - JSON.stringify({ providers: ["openai", "openai-codex"] }), + JSON.stringify({ + id: "openai", + configSchema: { type: "object" }, + providers: ["openai", "openai-codex"], + }), + ); + fs.writeFileSync( + path.join(pluginDir, "index.js"), + "export default { register() {} };\n", + "utf8", ); const resolveThinkingProfile = vi.fn(({ modelId }: { modelId: string }) => ({ @@ -52,6 +74,8 @@ describe("provider public artifacts", () => { vi.doMock("./bundled-dir.js", () => ({ resolveBundledPluginsDir: () => bundledPluginsDir, })); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledPluginsDir; + process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR = "1"; vi.doMock("./public-surface-loader.js", () => ({ loadBundledPluginPublicArtifactModuleSync, })); diff --git a/src/plugins/provider-public-artifacts.ts b/src/plugins/provider-public-artifacts.ts index 41d5f73f322..1b81da19aa9 100644 --- a/src/plugins/provider-public-artifacts.ts +++ b/src/plugins/provider-public-artifacts.ts @@ -1,9 +1,8 @@ -import fs from "node:fs"; -import path from "node:path"; import { normalizeProviderId } from "../agents/provider-id.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; +import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { ProviderApplyConfigDefaultsContext, ProviderNormalizeConfigContext, @@ -76,34 +75,24 @@ function resolveBundledProviderPolicyPluginId(providerId: string): string | null return providerPolicyPluginIdsByProviderId.get(cacheKey) ?? null; } - if (!bundledPluginsDir || !fs.existsSync(bundledPluginsDir)) { + if (!bundledPluginsDir) { providerPolicyPluginIdsByProviderId.set(cacheKey, null); return null; } - for (const entry of fs - .readdirSync(bundledPluginsDir, { withFileTypes: true }) - .filter((candidate) => candidate.isDirectory()) - .map((candidate) => candidate.name) - .toSorted((left, right) => left.localeCompare(right))) { - const manifestPath = path.join(bundledPluginsDir, entry, "openclaw.plugin.json"); - if (!fs.existsSync(manifestPath)) { + const registry = loadPluginManifestRegistry(); + for (const plugin of registry.plugins.toSorted((left, right) => + left.id.localeCompare(right.id), + )) { + if (plugin.origin !== "bundled") { continue; } - let manifest: { providers?: unknown }; - try { - manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as { providers?: unknown }; - } catch { - continue; - } - const providers = Array.isArray(manifest.providers) ? manifest.providers : []; - const ownsProvider = providers.some( - (candidate) => - typeof candidate === "string" && normalizeProviderId(candidate) === normalizedProviderId, + const ownsProvider = plugin.providers.some( + (provider) => normalizeProviderId(provider) === normalizedProviderId, ); if (ownsProvider) { - providerPolicyPluginIdsByProviderId.set(cacheKey, entry); - return entry; + providerPolicyPluginIdsByProviderId.set(cacheKey, plugin.id); + return plugin.id; } } diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 42af9f7a7e9..d9d8e3b6a40 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -1,4 +1,3 @@ -import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js"; @@ -70,16 +69,14 @@ function listBundledWebProviderSecretTargetRegistryEntries(): SecretTargetRegist function listBundledPluginConfigSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] { const entries: SecretTargetRegistryEntry[] = []; const seen = new Set(); - for (const record of listBundledPluginMetadata({ - includeChannelConfigs: false, - includeSyntheticChannelConfigs: false, - })) { - const secretInputs = record.manifest.configContracts?.secretInputs?.paths ?? []; + for (const record of loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }) + .plugins) { + if (record.origin !== "bundled") { + continue; + } + const secretInputs = record.configContracts?.secretInputs?.paths ?? []; for (const secretInput of secretInputs) { - const entry = createPluginOpenClawConfigSecretTargetEntry( - record.manifest.id, - secretInput.path, - ); + const entry = createPluginOpenClawConfigSecretTargetEntry(record.id, secretInput.path); const key = `${entry.configFile}:${entry.pathPattern}`; if (seen.has(key)) { continue;