From 25ce2e853f9645f4b7db6166f473788459c82460 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 09:28:38 +0100 Subject: [PATCH] refactor: unify plugin metadata consumers --- src/infra/provider-usage.auth.ts | 7 +- src/media-understanding/manifest-metadata.ts | 16 +-- .../channel-inbound-roots.fast-path.test.ts | 34 ++++++ src/media/channel-inbound-roots.ts | 17 ++- .../bundled-manifest-contract-plugins.ts | 92 +++++++++++++++ .../document-extractors.runtime.test.ts | 38 +----- src/plugins/document-extractors.runtime.ts | 108 ++---------------- src/plugins/effective-plugin-ids.ts | 7 +- .../manifest-command-aliases.runtime.ts | 7 +- .../manifest-contract-eligibility.test.ts | 7 +- src/plugins/manifest-contract-eligibility.ts | 31 ++++- .../manifest-model-suppression.test.ts | 51 +++++---- src/plugins/manifest-model-suppression.ts | 17 ++- src/plugins/web-content-extractors.runtime.ts | 108 ++---------------- ...provider-public-artifacts.fallback.test.ts | 31 ----- src/plugins/web-provider-public-artifacts.ts | 5 +- 16 files changed, 242 insertions(+), 334 deletions(-) create mode 100644 src/plugins/bundled-manifest-contract-plugins.ts diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 215aaf137a1..4d4bb414dec 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -13,12 +13,12 @@ import { resolveUsableCustomProviderApiKey } from "../agents/model-auth.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { getRuntimeConfig, type OpenClawConfig } from "../config/config.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; +import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js"; import { isActivatedManifestOwner, passesManifestOwnerBasePolicy, } from "../plugins/manifest-owner-policy.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js"; import { resolveProviderAuthEnvVarCandidates } from "../secrets/provider-env-vars.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; @@ -181,12 +181,11 @@ function resolveUsageCredentialProviderIds(params: { const providerIds = new Set(normalizeProviderIds([params.provider])); const providerIdSet = new Set(providerIds); try { - const registry = loadPluginManifestRegistryForPluginRegistry({ + const snapshot = loadManifestMetadataSnapshot({ config: params.state.cfg, env: params.state.env, - includeDisabled: true, }); - for (const plugin of registry.plugins) { + for (const plugin of snapshot.plugins) { const pluginProviderIds = normalizeProviderIds(plugin.providers); if (!pluginProviderIds.some((providerId) => providerIdSet.has(providerId))) { continue; diff --git a/src/media-understanding/manifest-metadata.ts b/src/media-understanding/manifest-metadata.ts index be77e09c6e7..1dec2879249 100644 --- a/src/media-understanding/manifest-metadata.ts +++ b/src/media-understanding/manifest-metadata.ts @@ -1,6 +1,5 @@ import type { OpenClawConfig } from "../config/types.js"; -import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; +import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js"; import { normalizeMediaProviderId } from "./provider-id.js"; import type { MediaUnderstandingProvider } from "./types.js"; @@ -9,19 +8,12 @@ export function buildMediaUnderstandingManifestMetadataRegistry( workspaceDir?: string, ): Map { const registry = new Map(); - const snapshot = getCurrentPluginMetadataSnapshot({ + const snapshot = loadManifestMetadataSnapshot({ config: cfg, + env: process.env, ...(workspaceDir ? { workspaceDir } : {}), }); - const plugins = - snapshot?.plugins ?? - loadPluginManifestRegistryForPluginRegistry({ - config: cfg, - env: process.env, - includeDisabled: true, - ...(workspaceDir ? { workspaceDir } : {}), - }).plugins; - for (const plugin of plugins) { + for (const plugin of snapshot.plugins) { const declaredProviders = new Set( (plugin.contracts?.mediaUnderstandingProviders ?? []).map((providerId) => normalizeMediaProviderId(providerId), diff --git a/src/media/channel-inbound-roots.fast-path.test.ts b/src/media/channel-inbound-roots.fast-path.test.ts index 2174dcb4245..c053d8aefab 100644 --- a/src/media/channel-inbound-roots.fast-path.test.ts +++ b/src/media/channel-inbound-roots.fast-path.test.ts @@ -68,6 +68,9 @@ describe("channel inbound roots fast path", () => { ctx: createContext("localchat"), }), ).toEqual(["/remote/work"]); + expect( + publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync, + ).toHaveBeenCalledOnce(); expect(publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync).toHaveBeenCalledWith( { dirName: "localchat", @@ -108,4 +111,35 @@ describe("channel inbound roots fast path", () => { artifactBasename: "index.js", }); }); + + it("preserves partial media contract modules when a missing resolver is checked first", () => { + publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync.mockImplementation( + ({ artifactBasename, dirName }: { artifactBasename: string; dirName: string }) => { + if (dirName === "partialchat" && artifactBasename === "media-contract-api.js") { + return { + resolveInboundAttachmentRoots: ({ accountId }: { accountId?: string }) => [ + `/partial/${accountId}`, + ], + }; + } + throw unableToResolve(dirName, artifactBasename); + }, + ); + + expect( + resolveChannelRemoteInboundAttachmentRoots({ + cfg, + ctx: createContext("partialchat"), + }), + ).toBeUndefined(); + expect( + resolveChannelInboundAttachmentRoots({ + cfg, + ctx: createContext("partialchat"), + }), + ).toEqual(["/partial/work"]); + expect( + publicSurfaceLoaderMocks.loadBundledPluginPublicArtifactModuleSync, + ).toHaveBeenCalledOnce(); + }); }); diff --git a/src/media/channel-inbound-roots.ts b/src/media/channel-inbound-roots.ts index 8698cedca3d..8b80c6bee40 100644 --- a/src/media/channel-inbound-roots.ts +++ b/src/media/channel-inbound-roots.ts @@ -15,19 +15,15 @@ type ChannelMediaContractApi = { }; type ChannelMediaRootResolver = keyof ChannelMediaContractApi; -const mediaContractApiByResolver = new Map(); - -function mediaContractCacheKey(channelId: string, resolver: ChannelMediaRootResolver): string { - return `${channelId}:${resolver}`; -} +const mediaContractApiByChannel = new Map(); function loadChannelMediaContractApi( channelId: string, resolver: ChannelMediaRootResolver, ): ChannelMediaContractApi | undefined { - const cacheKey = mediaContractCacheKey(channelId, resolver); - if (mediaContractApiByResolver.has(cacheKey)) { - return mediaContractApiByResolver.get(cacheKey) ?? undefined; + if (mediaContractApiByChannel.has(channelId)) { + const cached = mediaContractApiByChannel.get(channelId); + return cached && typeof cached[resolver] === "function" ? cached : undefined; } try { @@ -35,10 +31,11 @@ function loadChannelMediaContractApi( dirName: channelId, artifactBasename: "media-contract-api.js", }); + mediaContractApiByChannel.set(channelId, loaded); if (typeof loaded[resolver] === "function") { - mediaContractApiByResolver.set(cacheKey, loaded); return loaded; } + return undefined; } catch (error) { if ( !( @@ -50,7 +47,7 @@ function loadChannelMediaContractApi( } } - mediaContractApiByResolver.set(cacheKey, null); + mediaContractApiByChannel.set(channelId, null); return undefined; } diff --git a/src/plugins/bundled-manifest-contract-plugins.ts b/src/plugins/bundled-manifest-contract-plugins.ts new file mode 100644 index 00000000000..065ee321420 --- /dev/null +++ b/src/plugins/bundled-manifest-contract-plugins.ts @@ -0,0 +1,92 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + resolveBundledPluginCompatibleLoadValues, + type PluginActivationBundledCompatMode, +} from "./activation-context.js"; +import { + createPluginActivationSource, + normalizePluginsConfig, + resolveEffectivePluginActivationState, +} from "./config-state.js"; +import { loadManifestContractSnapshot } from "./manifest-contract-eligibility.js"; +import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js"; + +function createPluginIdSet(pluginIds: readonly string[] | undefined): Set | null { + return pluginIds && pluginIds.length > 0 ? new Set(pluginIds) : null; +} + +export function listBundledManifestContractPluginIds(params: { + plugins: readonly PluginManifestRecord[]; + contract: PluginManifestContractListKey; + onlyPluginIds?: readonly string[]; +}): string[] { + const onlyPluginIdSet = createPluginIdSet(params.onlyPluginIds); + return params.plugins + .filter( + (plugin) => + plugin.origin === "bundled" && + (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && + (plugin.contracts?.[params.contract]?.length ?? 0) > 0, + ) + .map((plugin) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); +} + +export function resolveEnabledBundledManifestContractPlugins(params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + onlyPluginIds?: readonly string[]; + contract: PluginManifestContractListKey; + compatMode: PluginActivationBundledCompatMode; +}): PluginManifestRecord[] { + if (params.config?.plugins?.enabled === false) { + return []; + } + let manifestRecords: readonly PluginManifestRecord[] | undefined; + const loadManifestRecords = (config?: OpenClawConfig) => { + manifestRecords ??= loadManifestContractSnapshot({ + config, + workspaceDir: params.workspaceDir, + env: params.env, + }).plugins; + return manifestRecords; + }; + + const activation = resolveBundledPluginCompatibleLoadValues({ + rawConfig: params.config, + env: params.env, + workspaceDir: params.workspaceDir, + onlyPluginIds: params.onlyPluginIds, + applyAutoEnable: true, + compatMode: params.compatMode, + resolveCompatPluginIds: (compatParams) => + listBundledManifestContractPluginIds({ + plugins: loadManifestRecords(compatParams.config), + contract: params.contract, + onlyPluginIds: compatParams.onlyPluginIds, + }), + }); + const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins); + const activationSource = createPluginActivationSource({ + config: activation.activationSourceConfig, + }); + const onlyPluginIdSet = createPluginIdSet(params.onlyPluginIds); + return loadManifestRecords(activation.config).filter((plugin) => { + if ( + plugin.origin !== "bundled" || + (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) || + (plugin.contracts?.[params.contract]?.length ?? 0) === 0 + ) { + return false; + } + return resolveEffectivePluginActivationState({ + id: plugin.id, + origin: plugin.origin, + config: normalizedPlugins, + rootConfig: activation.config, + enabledByDefault: plugin.enabledByDefault, + activationSource, + }).enabled; + }); +} diff --git a/src/plugins/document-extractors.runtime.test.ts b/src/plugins/document-extractors.runtime.test.ts index 8fc328d06cc..ec984906a98 100644 --- a/src/plugins/document-extractors.runtime.test.ts +++ b/src/plugins/document-extractors.runtime.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { resolvePluginDocumentExtractors } from "./document-extractors.runtime.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; vi.mock("./document-extractor-public-artifacts.js", () => ({ loadBundledDocumentExtractorEntriesFromDir: vi.fn( @@ -19,36 +19,8 @@ vi.mock("./document-extractor-public-artifacts.js", () => ({ ), })); -vi.mock("./manifest-registry-installed.js", () => ({ - loadPluginManifestRegistryForInstalledIndex: vi.fn(() => ({ - plugins: [ - { - id: "document-extract", - origin: "bundled", - enabledByDefault: true, - channels: [], - cliBackends: [], - providers: [], - legacyPluginIds: [], - contracts: { documentExtractors: ["pdf"] }, - }, - { - id: "openai", - origin: "bundled", - enabledByDefault: true, - channels: [], - cliBackends: [], - providers: ["openai", "openai-codex"], - legacyPluginIds: [], - contracts: {}, - }, - ], - })), -})); - -vi.mock("./plugin-registry.js", () => ({ - loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })), - loadPluginManifestRegistryForPluginRegistry: vi.fn(() => ({ +vi.mock("./plugin-metadata-snapshot.js", () => ({ + loadPluginMetadataSnapshot: vi.fn(() => ({ plugins: [ { id: "document-extract", @@ -80,10 +52,10 @@ vi.mock("./manifest-registry.js", () => ({ describe("resolvePluginDocumentExtractors", () => { it("reuses one manifest registry pass for compat and enabled bundled extractors", () => { - vi.mocked(loadPluginManifestRegistryForPluginRegistry).mockClear(); + vi.mocked(loadPluginMetadataSnapshot).mockClear(); expect(resolvePluginDocumentExtractors().map((extractor) => extractor.id)).toEqual(["pdf"]); - expect(loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledOnce(); + expect(loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); }); it("respects global plugin disablement", () => { diff --git a/src/plugins/document-extractors.runtime.ts b/src/plugins/document-extractors.runtime.ts index 58632501357..ee6ed99d06b 100644 --- a/src/plugins/document-extractors.runtime.ts +++ b/src/plugins/document-extractors.runtime.ts @@ -1,14 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveBundledPluginCompatibleLoadValues } from "./activation-context.js"; -import { - createPluginActivationSource, - normalizePluginsConfig, - resolveEffectivePluginActivationState, -} from "./config-state.js"; +import { resolveEnabledBundledManifestContractPlugins } from "./bundled-manifest-contract-plugins.js"; import { loadBundledDocumentExtractorEntriesFromDir } from "./document-extractor-public-artifacts.js"; import type { PluginDocumentExtractorEntry } from "./document-extractor-types.js"; -import type { PluginManifestRecord } from "./manifest-registry.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; function compareExtractors( left: PluginDocumentExtractorEntry, @@ -22,97 +15,6 @@ function compareExtractors( return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId); } -function listDocumentExtractorPluginIds(params: { - plugins: readonly PluginManifestRecord[]; - onlyPluginIds?: readonly string[]; -}): string[] { - const onlyPluginIdSet = - params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return params.plugins - .filter( - (plugin) => - plugin.origin === "bundled" && - (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && - (plugin.contracts?.documentExtractors?.length ?? 0) > 0, - ) - .map((plugin) => plugin.id) - .toSorted((left, right) => left.localeCompare(right)); -} - -function loadDocumentExtractorManifestRecords(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; -}): readonly PluginManifestRecord[] { - return loadPluginManifestRegistryForPluginRegistry({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }).plugins; -} - -function resolveEnabledBundledDocumentExtractorPlugins(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; - onlyPluginIds?: readonly string[]; -}): PluginManifestRecord[] { - if (params.config?.plugins?.enabled === false) { - return []; - } - let manifestRecords: readonly PluginManifestRecord[] | undefined; - const loadManifestRecords = (config?: OpenClawConfig) => { - manifestRecords ??= loadDocumentExtractorManifestRecords({ - config, - workspaceDir: params.workspaceDir, - env: params.env, - }); - return manifestRecords; - }; - - const activation = resolveBundledPluginCompatibleLoadValues({ - rawConfig: params.config, - env: params.env, - workspaceDir: params.workspaceDir, - onlyPluginIds: params.onlyPluginIds, - applyAutoEnable: true, - compatMode: { - allowlist: false, - enablement: "allowlist", - vitest: true, - }, - resolveCompatPluginIds: (compatParams) => - listDocumentExtractorPluginIds({ - plugins: loadManifestRecords(compatParams.config), - onlyPluginIds: compatParams.onlyPluginIds, - }), - }); - const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins); - const activationSource = createPluginActivationSource({ - config: activation.activationSourceConfig, - }); - const onlyPluginIdSet = - params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadManifestRecords(activation.config).filter((plugin) => { - if ( - plugin.origin !== "bundled" || - (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) || - (plugin.contracts?.documentExtractors?.length ?? 0) === 0 - ) { - return false; - } - return resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedPlugins, - rootConfig: activation.config, - enabledByDefault: plugin.enabledByDefault, - activationSource, - }).enabled; - }); -} - function resolveExplicitAllowedDocumentExtractorPluginIds(params: { config?: OpenClawConfig; onlyPluginIds?: readonly string[]; @@ -151,11 +53,17 @@ export function resolvePluginDocumentExtractors(params?: { }); const pluginIds = explicitAllowedPluginIds ?? - resolveEnabledBundledDocumentExtractorPlugins({ + resolveEnabledBundledManifestContractPlugins({ config: params?.config, workspaceDir: params?.workspaceDir, env: params?.env, onlyPluginIds: params?.onlyPluginIds, + contract: "documentExtractors", + compatMode: { + allowlist: false, + enablement: "allowlist", + vitest: true, + }, }).map((plugin) => plugin.id); for (const pluginId of pluginIds) { let loaded: PluginDocumentExtractorEntry[] | null; diff --git a/src/plugins/effective-plugin-ids.ts b/src/plugins/effective-plugin-ids.ts index 7daefd72548..83a5dba1748 100644 --- a/src/plugins/effective-plugin-ids.ts +++ b/src/plugins/effective-plugin-ids.ts @@ -11,8 +11,8 @@ import { resolveConfiguredChannelPluginIds, } from "./channel-plugin-ids.js"; import { normalizePluginsConfig } from "./config-state.js"; +import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js"; import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; function collectConfiguredChannelIds( config: OpenClawConfig, @@ -63,14 +63,13 @@ function collectBundledChannelOwnerPluginIds(params: { : {}), } : params.env; - const registry = loadPluginManifestRegistryForPluginRegistry({ + const snapshot = loadManifestMetadataSnapshot({ config: params.config, env, workspaceDir: params.workspaceDir, - includeDisabled: true, }); const pluginIds = new Set(); - for (const plugin of registry.plugins) { + for (const plugin of snapshot.plugins) { if (plugin.origin !== "bundled") { continue; } diff --git a/src/plugins/manifest-command-aliases.runtime.ts b/src/plugins/manifest-command-aliases.runtime.ts index d881761a4e5..fdacec73f3f 100644 --- a/src/plugins/manifest-command-aliases.runtime.ts +++ b/src/plugins/manifest-command-aliases.runtime.ts @@ -4,7 +4,7 @@ import { type PluginManifestCommandAliasRegistry, type PluginManifestCommandAliasRecord, } from "./manifest-command-aliases.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; +import { loadManifestMetadataRegistry } from "./manifest-contract-eligibility.js"; export function resolveManifestCommandAliasOwner(params: { command: string | undefined; @@ -15,12 +15,11 @@ export function resolveManifestCommandAliasOwner(params: { }): PluginManifestCommandAliasRecord | undefined { const registry = params.registry ?? - loadPluginManifestRegistryForPluginRegistry({ + loadManifestMetadataRegistry({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - includeDisabled: true, - }); + }).manifestRegistry; return resolveManifestCommandAliasOwnerInRegistry({ command: params.command, registry, diff --git a/src/plugins/manifest-contract-eligibility.test.ts b/src/plugins/manifest-contract-eligibility.test.ts index f030f0725d7..3fc82e14c7b 100644 --- a/src/plugins/manifest-contract-eligibility.test.ts +++ b/src/plugins/manifest-contract-eligibility.test.ts @@ -33,9 +33,10 @@ describe("loadManifestContractSnapshot", () => { }; mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(current); - expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toBe( - current, - ); + expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toEqual({ + index: current.index, + plugins: current.plugins, + }); expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ config: {}, diff --git a/src/plugins/manifest-contract-eligibility.ts b/src/plugins/manifest-contract-eligibility.ts index c7a0c3ce2ca..7813324c96d 100644 --- a/src/plugins/manifest-contract-eligibility.ts +++ b/src/plugins/manifest-contract-eligibility.ts @@ -5,6 +5,7 @@ import type { PluginManifestContractListKey, PluginManifestRecord } from "./mani import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; import type { PluginMetadataManifestView, + PluginMetadataRegistryView, PluginMetadataSnapshot, } from "./plugin-metadata-snapshot.types.js"; @@ -68,6 +69,30 @@ export function loadManifestContractSnapshot(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; }): PluginMetadataManifestView { + const snapshot = loadManifestMetadataSnapshot(params); + return { + index: snapshot.index, + plugins: snapshot.plugins, + }; +} + +export function loadManifestMetadataRegistry(params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): PluginMetadataRegistryView { + const snapshot = loadManifestMetadataSnapshot(params); + return { + index: snapshot.index, + manifestRegistry: snapshot.manifestRegistry, + }; +} + +export function loadManifestMetadataSnapshot(params: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): PluginMetadataSnapshot { const env = params.env ?? process.env; const current = getCurrentPluginMetadataSnapshot({ config: params.config, @@ -77,13 +102,9 @@ export function loadManifestContractSnapshot(params: { if (current) { return current; } - const snapshot = loadPluginMetadataSnapshot({ + return loadPluginMetadataSnapshot({ config: params.config ?? {}, env, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); - return { - index: snapshot.index, - plugins: snapshot.plugins, - }; } diff --git a/src/plugins/manifest-model-suppression.test.ts b/src/plugins/manifest-model-suppression.test.ts index 2e30fedf647..4b7549766ad 100644 --- a/src/plugins/manifest-model-suppression.test.ts +++ b/src/plugins/manifest-model-suppression.test.ts @@ -1,11 +1,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ - loadPluginManifestRegistryForPluginRegistry: vi.fn(), + loadPluginMetadataSnapshot: vi.fn(), })); -vi.mock("./plugin-registry.js", () => ({ - loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry, +vi.mock("./plugin-metadata-snapshot.js", () => ({ + loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot, })); import { @@ -13,12 +13,19 @@ import { resolveManifestBuiltInModelSuppression, } from "./manifest-model-suppression.js"; +function createMetadataSnapshot(plugins: Record[]) { + return { + index: { plugins: [] }, + diagnostics: [], + plugins: plugins.map((plugin) => ({ origin: "bundled", ...plugin })), + }; +} + describe("manifest model suppression", () => { beforeEach(() => { - mocks.loadPluginManifestRegistryForPluginRegistry.mockReset(); - mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ - diagnostics: [], - plugins: [ + mocks.loadPluginMetadataSnapshot.mockReset(); + mocks.loadPluginMetadataSnapshot.mockReturnValue( + createMetadataSnapshot([ { id: "openai", providers: ["openai"], @@ -41,8 +48,8 @@ describe("manifest model suppression", () => { ], }, }, - ], - }); + ]), + ); }); describe("buildManifestBuiltInModelSuppressionResolver", () => { @@ -54,7 +61,7 @@ describe("manifest model suppression", () => { env: process.env, }); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1); + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1); resolver({ provider: "azure-openai-responses", @@ -65,7 +72,7 @@ describe("manifest model suppression", () => { id: "gpt-5.3-codex-spark", }); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1); + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1); }); }); @@ -109,7 +116,7 @@ describe("manifest model suppression", () => { env: process.env, }); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(2); + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(2); }); it("reuses planned manifest suppressions inside a resolver instance", () => { @@ -132,13 +139,12 @@ describe("manifest model suppression", () => { id: "gpt-4.1", }), ).toBeUndefined(); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1); + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledTimes(1); }); it("matches conditional suppressions by base URL host", () => { - mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ - diagnostics: [], - plugins: [ + mocks.loadPluginMetadataSnapshot.mockReturnValue( + createMetadataSnapshot([ { id: "qwen", providers: ["qwen", "modelstudio"], @@ -159,8 +165,8 @@ describe("manifest model suppression", () => { ], }, }, - ], - }); + ]), + ); expect( resolveManifestBuiltInModelSuppression({ @@ -189,9 +195,8 @@ describe("manifest model suppression", () => { }); it("does not apply conditional suppressions to custom providers with a foreign api owner", () => { - mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ - diagnostics: [], - plugins: [ + mocks.loadPluginMetadataSnapshot.mockReturnValue( + createMetadataSnapshot([ { id: "qwen", providers: ["modelstudio"], @@ -208,8 +213,8 @@ describe("manifest model suppression", () => { ], }, }, - ], - }); + ]), + ); expect( resolveManifestBuiltInModelSuppression({ diff --git a/src/plugins/manifest-model-suppression.ts b/src/plugins/manifest-model-suppression.ts index 18df4e90215..766e6510247 100644 --- a/src/plugins/manifest-model-suppression.ts +++ b/src/plugins/manifest-model-suppression.ts @@ -5,18 +5,31 @@ import { type ManifestModelCatalogSuppressionEntry, } from "../model-catalog/index.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; +import { + isManifestPluginAvailableForControlPlane, + loadManifestMetadataSnapshot, +} from "./manifest-contract-eligibility.js"; function listManifestModelCatalogSuppressions(params: { config?: OpenClawConfig; workspaceDir?: string; env: NodeJS.ProcessEnv; }): readonly ManifestModelCatalogSuppressionEntry[] { - const registry = loadPluginManifestRegistryForPluginRegistry({ + const snapshot = loadManifestMetadataSnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, }); + const registry = { + diagnostics: snapshot.diagnostics, + plugins: snapshot.plugins.filter((plugin) => + isManifestPluginAvailableForControlPlane({ + snapshot, + plugin, + config: params.config, + }), + ), + }; const planned = planManifestModelCatalogSuppressions({ registry }); return planned.suppressions; } diff --git a/src/plugins/web-content-extractors.runtime.ts b/src/plugins/web-content-extractors.runtime.ts index a1e14908a20..faf96db195b 100644 --- a/src/plugins/web-content-extractors.runtime.ts +++ b/src/plugins/web-content-extractors.runtime.ts @@ -1,12 +1,5 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveBundledPluginCompatibleLoadValues } from "./activation-context.js"; -import { - createPluginActivationSource, - normalizePluginsConfig, - resolveEffectivePluginActivationState, -} from "./config-state.js"; -import type { PluginManifestRecord } from "./manifest-registry.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; +import { resolveEnabledBundledManifestContractPlugins } from "./bundled-manifest-contract-plugins.js"; import { loadBundledWebContentExtractorEntriesFromDir } from "./web-content-extractor-public-artifacts.js"; import type { PluginWebContentExtractorEntry } from "./web-content-extractor-types.js"; @@ -22,97 +15,6 @@ function compareExtractors( return left.id.localeCompare(right.id) || left.pluginId.localeCompare(right.pluginId); } -function listWebContentExtractorPluginIds(params: { - plugins: readonly PluginManifestRecord[]; - onlyPluginIds?: readonly string[]; -}): string[] { - const onlyPluginIdSet = - params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return params.plugins - .filter( - (plugin) => - plugin.origin === "bundled" && - (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && - (plugin.contracts?.webContentExtractors?.length ?? 0) > 0, - ) - .map((plugin) => plugin.id) - .toSorted((left, right) => left.localeCompare(right)); -} - -function loadWebContentExtractorManifestRecords(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; -}): readonly PluginManifestRecord[] { - return loadPluginManifestRegistryForPluginRegistry({ - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, - }).plugins; -} - -function resolveEnabledBundledExtractorPlugins(params: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; - onlyPluginIds?: readonly string[]; -}): PluginManifestRecord[] { - if (params.config?.plugins?.enabled === false) { - return []; - } - let manifestRecords: readonly PluginManifestRecord[] | undefined; - const loadManifestRecords = (config?: OpenClawConfig) => { - manifestRecords ??= loadWebContentExtractorManifestRecords({ - config, - workspaceDir: params.workspaceDir, - env: params.env, - }); - return manifestRecords; - }; - - const activation = resolveBundledPluginCompatibleLoadValues({ - rawConfig: params.config, - env: params.env, - workspaceDir: params.workspaceDir, - onlyPluginIds: params.onlyPluginIds, - applyAutoEnable: true, - compatMode: { - allowlist: true, - enablement: "always", - vitest: true, - }, - resolveCompatPluginIds: (compatParams) => - listWebContentExtractorPluginIds({ - plugins: loadManifestRecords(compatParams.config), - onlyPluginIds: compatParams.onlyPluginIds, - }), - }); - const normalizedPlugins = normalizePluginsConfig(activation.config?.plugins); - const activationSource = createPluginActivationSource({ - config: activation.activationSourceConfig, - }); - const onlyPluginIdSet = - params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null; - return loadManifestRecords(activation.config).filter((plugin) => { - if ( - plugin.origin !== "bundled" || - (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.id)) || - (plugin.contracts?.webContentExtractors?.length ?? 0) === 0 - ) { - return false; - } - return resolveEffectivePluginActivationState({ - id: plugin.id, - origin: plugin.origin, - config: normalizedPlugins, - rootConfig: activation.config, - enabledByDefault: plugin.enabledByDefault, - activationSource, - }).enabled; - }); -} - export function resolvePluginWebContentExtractors(params?: { config?: OpenClawConfig; workspaceDir?: string; @@ -120,11 +22,17 @@ export function resolvePluginWebContentExtractors(params?: { onlyPluginIds?: readonly string[]; }): PluginWebContentExtractorEntry[] { const extractors: PluginWebContentExtractorEntry[] = []; - for (const plugin of resolveEnabledBundledExtractorPlugins({ + for (const plugin of resolveEnabledBundledManifestContractPlugins({ config: params?.config, workspaceDir: params?.workspaceDir, env: params?.env, onlyPluginIds: params?.onlyPluginIds, + contract: "webContentExtractors", + compatMode: { + allowlist: true, + enablement: "always", + vitest: true, + }, })) { const loaded = loadBundledWebContentExtractorEntriesFromDir({ dirName: plugin.id, diff --git a/src/plugins/web-provider-public-artifacts.fallback.test.ts b/src/plugins/web-provider-public-artifacts.fallback.test.ts index 1d2af06da42..d200de9fc46 100644 --- a/src/plugins/web-provider-public-artifacts.fallback.test.ts +++ b/src/plugins/web-provider-public-artifacts.fallback.test.ts @@ -1,20 +1,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ - loadPluginManifestRegistryForPluginRegistry: vi.fn(), loadPluginMetadataSnapshot: vi.fn(), - loadPluginRegistrySnapshotWithMetadata: vi.fn(), resolveBundledExplicitWebSearchProvidersFromPublicArtifacts: vi.fn(() => null), resolveBundledExplicitWebFetchProvidersFromPublicArtifacts: vi.fn(() => null), loadBundledWebSearchProviderEntriesFromDir: vi.fn(), loadBundledWebFetchProviderEntriesFromDir: vi.fn(), })); -vi.mock("./plugin-registry.js", () => ({ - loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry, - loadPluginRegistrySnapshotWithMetadata: mocks.loadPluginRegistrySnapshotWithMetadata, -})); - vi.mock("./plugin-metadata-snapshot.js", () => ({ loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot, })); @@ -48,23 +41,6 @@ const { describe("web provider public artifact manifest fallback", () => { beforeEach(() => { vi.clearAllMocks(); - mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ - diagnostics: [], - plugins: [ - { - id: "fallback-search", - origin: "bundled", - rootDir: "/tmp/fallback-search", - contracts: { webSearchProviders: ["fallback-search"] }, - }, - { - id: "fallback-fetch", - origin: "bundled", - rootDir: "/tmp/fallback-fetch", - contracts: { webFetchProviders: ["fallback-fetch"] }, - }, - ], - }); mocks.loadPluginMetadataSnapshot.mockReturnValue({ diagnostics: [], plugins: [ @@ -82,11 +58,6 @@ describe("web provider public artifact manifest fallback", () => { }, ], }); - mocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({ - source: "derived", - snapshot: { plugins: [] }, - diagnostics: [], - }); mocks.loadBundledWebSearchProviderEntriesFromDir.mockReturnValue([ { id: "fallback-search", pluginId: "fallback-search" }, ]); @@ -100,7 +71,6 @@ describe("web provider public artifact manifest fallback", () => { expect(providers).toEqual([{ id: "fallback-search", pluginId: "fallback-search" }]); expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).not.toHaveBeenCalled(); expect(mocks.loadBundledWebSearchProviderEntriesFromDir).toHaveBeenCalledWith({ dirName: "fallback-search", pluginId: "fallback-search", @@ -112,7 +82,6 @@ describe("web provider public artifact manifest fallback", () => { expect(providers).toEqual([{ id: "fallback-fetch", pluginId: "fallback-fetch" }]); expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); - expect(mocks.loadPluginManifestRegistryForPluginRegistry).not.toHaveBeenCalled(); expect(mocks.loadBundledWebFetchProviderEntriesFromDir).toHaveBeenCalledWith({ dirName: "fallback-fetch", pluginId: "fallback-fetch", diff --git a/src/plugins/web-provider-public-artifacts.ts b/src/plugins/web-provider-public-artifacts.ts index f7e85df3fbe..bcd89d29a0c 100644 --- a/src/plugins/web-provider-public-artifacts.ts +++ b/src/plugins/web-provider-public-artifacts.ts @@ -1,7 +1,7 @@ import path from "node:path"; import type { PluginLoadOptions } from "./loader.js"; +import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; import type { PluginWebFetchProviderEntry, PluginWebSearchProviderEntry } from "./types.js"; import { resolveBundledWebFetchResolutionConfig } from "./web-fetch-providers.shared.js"; import { @@ -71,11 +71,10 @@ function resolveBundledManifestRecordsByPluginId(params: { const allowedPluginIds = new Set(params.onlyPluginIds); const manifestRecords = params.manifestRecords ?? - loadPluginManifestRegistryForPluginRegistry({ + loadManifestMetadataSnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - includeDisabled: true, }).plugins; return new Map( manifestRecords