refactor: unify plugin metadata consumers

This commit is contained in:
Peter Steinberger
2026-05-02 09:28:38 +01:00
parent befd4124f7
commit 25ce2e853f
16 changed files with 242 additions and 334 deletions

View File

@@ -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;

View File

@@ -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<string, MediaUnderstandingProvider> {
const registry = new Map<string, MediaUnderstandingProvider>();
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),

View File

@@ -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();
});
});

View File

@@ -15,19 +15,15 @@ type ChannelMediaContractApi = {
};
type ChannelMediaRootResolver = keyof ChannelMediaContractApi;
const mediaContractApiByResolver = new Map<string, ChannelMediaContractApi | null>();
function mediaContractCacheKey(channelId: string, resolver: ChannelMediaRootResolver): string {
return `${channelId}:${resolver}`;
}
const mediaContractApiByChannel = new Map<string, ChannelMediaContractApi | null>();
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;
}

View File

@@ -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<string> | 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;
});
}

View File

@@ -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", () => {

View File

@@ -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;

View File

@@ -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<string>();
for (const plugin of registry.plugins) {
for (const plugin of snapshot.plugins) {
if (plugin.origin !== "bundled") {
continue;
}

View File

@@ -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,

View File

@@ -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: {},

View File

@@ -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,
};
}

View File

@@ -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<string, unknown>[]) {
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({

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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