refactor: route plugin metadata consumers through snapshots

This commit is contained in:
Peter Steinberger
2026-05-02 08:41:14 +01:00
parent cf35fa8e57
commit 2f44ffc8a7
18 changed files with 257 additions and 162 deletions

View File

@@ -11,9 +11,21 @@ import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js";
const hoisted = vi.hoisted(() => { const hoisted = vi.hoisted(() => {
const loadManifestRegistry = vi.fn(); const loadManifestRegistry = vi.fn();
const loadPluginMetadataSnapshot = vi.fn(() => {
const manifestRegistry = loadManifestRegistry();
return {
manifestRegistry,
plugins: manifestRegistry.plugins,
normalizePluginId: (pluginId: string) =>
manifestRegistry.plugins.find((plugin: { id: string; legacyPluginIds?: string[] }) =>
plugin.legacyPluginIds?.includes(pluginId),
)?.id ?? pluginId,
};
});
return { return {
loadPluginManifestRegistryForInstalledIndex: loadManifestRegistry, loadPluginManifestRegistryForInstalledIndex: loadManifestRegistry,
loadPluginManifestRegistryForPluginRegistry: loadManifestRegistry, loadPluginManifestRegistryForPluginRegistry: loadManifestRegistry,
loadPluginMetadataSnapshot,
loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })), loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })),
}; };
}); });
@@ -27,6 +39,10 @@ vi.mock("../../plugins/plugin-registry.js", () => ({
loadPluginRegistrySnapshot: hoisted.loadPluginRegistrySnapshot, loadPluginRegistrySnapshot: hoisted.loadPluginRegistrySnapshot,
})); }));
vi.mock("../../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginMetadataSnapshot: hoisted.loadPluginMetadataSnapshot,
}));
let resolvePluginSkillDirs: typeof import("./plugin-skills.js").resolvePluginSkillDirs; let resolvePluginSkillDirs: typeof import("./plugin-skills.js").resolvePluginSkillDirs;
const tempDirs = createTrackedTempDirs(); const tempDirs = createTrackedTempDirs();
@@ -135,6 +151,7 @@ function registerHealthyAcpBackend() {
afterEach(async () => { afterEach(async () => {
hoisted.loadPluginManifestRegistryForInstalledIndex.mockReset(); hoisted.loadPluginManifestRegistryForInstalledIndex.mockReset();
hoisted.loadPluginMetadataSnapshot.mockClear();
hoisted.loadPluginRegistrySnapshot.mockReset(); hoisted.loadPluginRegistrySnapshot.mockReset();
acpRuntimeTesting.resetAcpRuntimeBackendsForTests(); acpRuntimeTesting.resetAcpRuntimeBackendsForTests();
await tempDirs.cleanup(); await tempDirs.cleanup();
@@ -151,6 +168,7 @@ describe("resolvePluginSkillDirs", () => {
diagnostics: [], diagnostics: [],
plugins: [], plugins: [],
}); });
hoisted.loadPluginMetadataSnapshot.mockClear();
hoisted.loadPluginRegistrySnapshot.mockReset(); hoisted.loadPluginRegistrySnapshot.mockReset();
hoisted.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] }); hoisted.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] });
}); });

View File

@@ -8,40 +8,12 @@ import {
resolveEffectivePluginActivationState, resolveEffectivePluginActivationState,
resolveMemorySlotDecision, resolveMemorySlotDecision,
} from "../../plugins/config-policy.js"; } from "../../plugins/config-policy.js";
import type { PluginManifestRegistry } from "../../plugins/manifest-registry.js"; import { loadPluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
import { hasKind } from "../../plugins/slots.js"; import { hasKind } from "../../plugins/slots.js";
import { isPathInsideWithRealpath } from "../../security/scan-paths.js"; import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
const log = createSubsystemLogger("skills"); const log = createSubsystemLogger("skills");
function buildRegistryPluginIdAliases(
registry: PluginManifestRegistry,
): Readonly<Record<string, string>> {
return Object.fromEntries(
registry.plugins
.flatMap((record) => [
...record.providers
.filter((providerId) => providerId !== record.id)
.map((providerId) => [providerId, record.id] as const),
...(record.legacyPluginIds ?? []).map(
(legacyPluginId) => [legacyPluginId, record.id] as const,
),
])
.toSorted(([left], [right]) => left.localeCompare(right)),
);
}
function createRegistryPluginIdNormalizer(
registry: PluginManifestRegistry,
): (id: string) => string {
const aliases = buildRegistryPluginIdAliases(registry);
return (id: string) => {
const trimmed = id.trim();
return aliases[trimmed] ?? trimmed;
};
}
export function resolvePluginSkillDirs(params: { export function resolvePluginSkillDirs(params: {
workspaceDir: string | undefined; workspaceDir: string | undefined;
config?: OpenClawConfig; config?: OpenClawConfig;
@@ -50,17 +22,18 @@ export function resolvePluginSkillDirs(params: {
if (!workspaceDir) { if (!workspaceDir) {
return []; return [];
} }
const registry = loadPluginManifestRegistryForPluginRegistry({ const metadataSnapshot = loadPluginMetadataSnapshot({
workspaceDir, workspaceDir,
config: params.config, config: params.config ?? {},
includeDisabled: true, env: process.env,
}); });
const registry = metadataSnapshot.manifestRegistry;
if (registry.plugins.length === 0) { if (registry.plugins.length === 0) {
return []; return [];
} }
const normalizedPlugins = normalizePluginsConfigWithResolver( const normalizedPlugins = normalizePluginsConfigWithResolver(
params.config?.plugins, params.config?.plugins,
createRegistryPluginIdNormalizer(registry), metadataSnapshot.normalizePluginId,
); );
const acpRuntimeAvailable = isAcpRuntimeSpawnAvailable({ config: params.config }); const acpRuntimeAvailable = isAcpRuntimeSpawnAvailable({ config: params.config });
const memorySlot = normalizedPlugins.slots.memory; const memorySlot = normalizedPlugins.slots.memory;

View File

@@ -310,11 +310,11 @@ function createBundledChannelLoadContext(): BundledChannelLoadContext {
}; };
} }
function resolveActiveBundledChannelLoadScope(): { function resolveActiveBundledChannelLoadScope(env: NodeJS.ProcessEnv = process.env): {
rootScope: BundledChannelRootScope; rootScope: BundledChannelRootScope;
loadContext: BundledChannelLoadContext; loadContext: BundledChannelLoadContext;
} { } {
const rootScope = resolveBundledChannelRootScope(); const rootScope = resolveBundledChannelRootScope(env);
const cachedContext = bundledChannelLoadContextsByRoot.get(rootScope.cacheKey); const cachedContext = bundledChannelLoadContextsByRoot.get(rootScope.cacheKey);
if (cachedContext) { if (cachedContext) {
bundledChannelLoadContextsByRoot.delete(rootScope.cacheKey); bundledChannelLoadContextsByRoot.delete(rootScope.cacheKey);
@@ -787,13 +787,19 @@ export function getBundledChannelSecrets(id: ChannelId): ChannelPlugin["secrets"
return getBundledChannelSecretsForRoot(id, rootScope, loadContext); return getBundledChannelSecretsForRoot(id, rootScope, loadContext);
} }
export function getBundledChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined { export function getBundledChannelSetupPlugin(
const { rootScope, loadContext } = resolveActiveBundledChannelLoadScope(); id: ChannelId,
env: NodeJS.ProcessEnv = process.env,
): ChannelPlugin | undefined {
const { rootScope, loadContext } = resolveActiveBundledChannelLoadScope(env);
return getBundledChannelSetupPluginForRoot(id, rootScope, loadContext); return getBundledChannelSetupPluginForRoot(id, rootScope, loadContext);
} }
export function getBundledChannelSetupSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined { export function getBundledChannelSetupSecrets(
const { rootScope, loadContext } = resolveActiveBundledChannelLoadScope(); id: ChannelId,
env: NodeJS.ProcessEnv = process.env,
): ChannelPlugin["secrets"] | undefined {
const { rootScope, loadContext } = resolveActiveBundledChannelLoadScope(env);
return getBundledChannelSetupSecretsForRoot(id, rootScope, loadContext); return getBundledChannelSetupSecretsForRoot(id, rootScope, loadContext);
} }

View File

@@ -2,18 +2,24 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { isBlockedObjectKey } from "../../infra/prototype-keys.js"; import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { import {
hasExplicitChannelConfig, hasExplicitChannelConfig,
listConfiguredChannelIdsForReadOnlyScope, listConfiguredChannelIdsForReadOnlyScope,
resolveDiscoverableScopedChannelPluginIds, resolveDiscoverableScopedChannelPluginIds,
} from "../../plugins/channel-plugin-ids.js"; } from "../../plugins/channel-plugin-ids.js";
import {
channelPluginIdBelongsToManifest,
resolveSetupChannelRegistration,
} from "../../plugins/loader-channel-setup.js";
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js"; import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
import { loadPluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
import { import {
getCachedPluginModuleLoader, getCachedPluginModuleLoader,
type PluginModuleLoaderCache, type PluginModuleLoaderCache,
} from "../../plugins/plugin-module-loader-cache.js"; } from "../../plugins/plugin-module-loader-cache.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../plugins/plugin-registry.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { sanitizeForLog } from "../../terminal/ansi.js"; import { sanitizeForLog } from "../../terminal/ansi.js";
import { getBundledChannelSetupPlugin } from "./bundled.js"; import { getBundledChannelSetupPlugin } from "./bundled.js";
@@ -35,6 +41,7 @@ const BUILT_PLUGIN_LOADER_MODULE_CANDIDATES = [
"plugins/build-smoke-entry.js", "plugins/build-smoke-entry.js",
] as const; ] as const;
const moduleLoaders: PluginModuleLoaderCache = new Map(); const moduleLoaders: PluginModuleLoaderCache = new Map();
const log = createSubsystemLogger("channels");
type PluginLoaderModule = { type PluginLoaderModule = {
loadOpenClawPlugins: (params: { loadOpenClawPlugins: (params: {
@@ -366,6 +373,44 @@ function canUseManifestChannelPlugin(record: PluginManifestRecord, channelId: st
export { resolveReadOnlyChannelCommandDefaults }; export { resolveReadOnlyChannelCommandDefaults };
function loadSetupChannelPluginFromManifestRecord(params: {
record: PluginManifestRecord;
channelId: string;
}): ChannelPlugin | undefined {
if (!params.record.setupSource || !params.record.channels.includes(params.channelId)) {
return undefined;
}
try {
const moduleLoader = getCachedPluginModuleLoader({
cache: moduleLoaders,
modulePath: params.record.setupSource,
importerUrl: import.meta.url,
preferBuiltDist: true,
loaderFilename: import.meta.url,
tryNative: true,
cacheScopeKey: "read-only-setup-entry",
});
const registration = resolveSetupChannelRegistration(moduleLoader(params.record.setupSource));
if (!registration.plugin) {
return undefined;
}
if (
!channelPluginIdBelongsToManifest({
channelId: registration.plugin.id,
pluginId: params.record.id,
manifestChannels: params.record.channels,
})
) {
return undefined;
}
return cloneChannelPluginForChannelId(registration.plugin, params.channelId);
} catch (error) {
const detail = formatErrorMessage(error);
log.warn(`[channels] failed to load channel setup ${params.record.id}: ${detail}`);
return undefined;
}
}
function rebindChannelPluginConfig( function rebindChannelPluginConfig(
config: ChannelPlugin["config"], config: ChannelPlugin["config"],
sourceChannelId: string, sourceChannelId: string,
@@ -652,12 +697,11 @@ export function resolveReadOnlyChannelPluginsForConfig(
): ReadOnlyChannelPluginResolution { ): ReadOnlyChannelPluginResolution {
const env = options.env ?? process.env; const env = options.env ?? process.env;
const workspaceDir = resolveReadOnlyWorkspaceDir(cfg, options); const workspaceDir = resolveReadOnlyWorkspaceDir(cfg, options);
const manifestRecords = loadPluginManifestRegistryForPluginRegistry({ const manifestRecords = loadPluginMetadataSnapshot({
config: cfg, config: cfg,
stateDir: options.stateDir, stateDir: options.stateDir,
workspaceDir, workspaceDir,
env, env,
includeDisabled: true,
}).plugins; }).plugins;
const bundledManifestRecords = listBundledChannelManifestRecords(manifestRecords); const bundledManifestRecords = listBundledChannelManifestRecords(manifestRecords);
const externalManifestRecords = listExternalChannelManifestRecords(manifestRecords); const externalManifestRecords = listExternalChannelManifestRecords(manifestRecords);
@@ -682,7 +726,17 @@ export function resolveReadOnlyChannelPluginsForConfig(
if (byId.has(channelId)) { if (byId.has(channelId)) {
continue; continue;
} }
addChannelPlugins(byId, [getBundledChannelSetupPlugin(channelId)]); const bundledSetupPlugin =
bundledManifestRecords
.filter((record) => record.channels.includes(channelId))
.map((record) =>
loadSetupChannelPluginFromManifestRecord({
record,
channelId,
}),
)
.find((plugin) => plugin) ?? getBundledChannelSetupPlugin(channelId, env);
addChannelPlugins(byId, [bundledSetupPlugin]);
} }
} }

View File

@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js"; import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js"; import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
import type { PluginKind } from "../plugins/plugin-kind.types.js"; import type { PluginKind } from "../plugins/plugin-kind.types.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import { applyExclusiveSlotSelection } from "../plugins/slots.js"; import { applyExclusiveSlotSelection } from "../plugins/slots.js";
import { buildPluginDiagnosticsReport } from "../plugins/status.js"; import { buildPluginDiagnosticsReport } from "../plugins/status.js";
import type { PluginLogger } from "../plugins/types.js"; import type { PluginLogger } from "../plugins/types.js";
@@ -59,13 +59,12 @@ function buildSlotSelectionRegistry(
config: OpenClawConfig, config: OpenClawConfig,
pluginId: string, pluginId: string,
): SlotSelectionRegistry { ): SlotSelectionRegistry {
const registry = loadPluginManifestRegistryForPluginRegistry({ const plugins = loadPluginMetadataSnapshot({
config, config,
includeDisabled: true, env: process.env,
pluginIds: [pluginId], }).plugins.filter((plugin) => plugin.id === pluginId);
});
return { return {
plugins: registry.plugins.map((plugin) => ({ plugins: plugins.map((plugin) => ({
id: plugin.id, id: plugin.id,
kind: plugin.kind, kind: plugin.kind,
})), })),

View File

@@ -1,8 +1,8 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js"; import type { OpenClawConfig } from "../../../config/config.js";
vi.mock("../../../plugins/plugin-registry.js", () => ({ vi.mock("../../../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginManifestRegistryForPluginRegistry: () => ({ loadPluginMetadataSnapshot: () => ({
plugins: [ plugins: [
{ {
id: "brave", id: "brave",
@@ -21,8 +21,6 @@ vi.mock("../../../plugins/plugin-registry.js", () => ({
}, },
], ],
}), }),
resolveManifestContractOwnerPluginId: ({ value }: { value: string }) =>
({ brave: "brave", grok: "xai", kimi: "moonshot" })[value as "brave" | "grok" | "kimi"],
})); }));
import { import {

View File

@@ -1,8 +1,5 @@
import { mergeMissing } from "../../../config/legacy.shared.js"; import { mergeMissing } from "../../../config/legacy.shared.js";
import { import { loadPluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.js";
loadPluginManifestRegistryForPluginRegistry,
resolveManifestContractOwnerPluginId,
} from "../../../plugins/plugin-registry.js";
import { import {
cloneRecord, cloneRecord,
ensureRecord, ensureRecord,
@@ -18,18 +15,31 @@ const MODERN_SCOPED_WEB_SEARCH_KEYS = new Set(["openaiCodex"]);
const NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS = new Set(["tavily"]); const NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS = new Set(["tavily"]);
const LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID = "brave"; const LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID = "brave";
function getLegacyWebSearchProviderIds(): string[] { function getBundledLegacyWebSearchOwners(): ReadonlyMap<string, string> {
return loadPluginManifestRegistryForPluginRegistry({ const owners = new Map<string, string>();
includeDisabled: true, for (const plugin of loadPluginMetadataSnapshot({ config: {}, env: process.env }).plugins) {
}) if (plugin.origin !== "bundled") {
.plugins.filter((plugin) => plugin.origin === "bundled") continue;
.flatMap((plugin) => plugin.contracts?.webSearchProviders ?? []) }
for (const providerId of plugin.contracts?.webSearchProviders ?? []) {
if (!owners.has(providerId)) {
owners.set(providerId, plugin.id);
}
}
}
return owners;
}
function getLegacyWebSearchProviderIds(
owners: ReadonlyMap<string, string> = getBundledLegacyWebSearchOwners(),
): string[] {
return [...owners.keys()]
.filter((providerId) => !NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS.has(providerId)) .filter((providerId) => !NON_MIGRATED_LEGACY_WEB_SEARCH_PROVIDER_IDS.has(providerId))
.toSorted((left, right) => left.localeCompare(right)); .toSorted((left, right) => left.localeCompare(right));
} }
function getLegacyWebSearchProviderIdSet(): Set<string> { function getLegacyWebSearchProviderIdSet(owners: ReadonlyMap<string, string>): Set<string> {
return new Set(getLegacyWebSearchProviderIds()); return new Set(getLegacyWebSearchProviderIds(owners));
} }
function resolveLegacySearchConfig(raw: unknown): JsonRecord | undefined { function resolveLegacySearchConfig(raw: unknown): JsonRecord | undefined {
@@ -46,7 +56,10 @@ function copyLegacyProviderConfig(search: JsonRecord, providerKey: string): Json
return isRecord(current) ? cloneRecord(current) : undefined; return isRecord(current) ? cloneRecord(current) : undefined;
} }
function hasMappedLegacyWebSearchConfig(raw: unknown): boolean { function hasMappedLegacyWebSearchConfig(
raw: unknown,
owners: ReadonlyMap<string, string>,
): boolean {
const search = resolveLegacySearchConfig(raw); const search = resolveLegacySearchConfig(raw);
if (!search) { if (!search) {
return false; return false;
@@ -54,10 +67,13 @@ function hasMappedLegacyWebSearchConfig(raw: unknown): boolean {
if (hasOwnKey(search, "apiKey")) { if (hasOwnKey(search, "apiKey")) {
return true; return true;
} }
return getLegacyWebSearchProviderIds().some((providerId) => isRecord(search[providerId])); return getLegacyWebSearchProviderIds(owners).some((providerId) => isRecord(search[providerId]));
} }
function resolveLegacyGlobalWebSearchMigration(search: JsonRecord): { function resolveLegacyGlobalWebSearchMigration(
search: JsonRecord,
owners: ReadonlyMap<string, string>,
): {
pluginId: string; pluginId: string;
payload: JsonRecord; payload: JsonRecord;
legacyPath: string; legacyPath: string;
@@ -76,11 +92,7 @@ function resolveLegacyGlobalWebSearchMigration(search: JsonRecord): {
return null; return null;
} }
const pluginId = const pluginId =
resolveManifestContractOwnerPluginId({ owners.get(LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID) ?? LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID;
contract: "webSearchProviders",
value: LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID,
origin: "bundled",
}) ?? LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID;
return { return {
pluginId, pluginId,
payload, payload,
@@ -134,6 +146,7 @@ function migratePluginWebSearchConfig(params: {
} }
export function listLegacyWebSearchConfigPaths(raw: unknown): string[] { export function listLegacyWebSearchConfigPaths(raw: unknown): string[] {
const owners = getBundledLegacyWebSearchOwners();
const search = resolveLegacySearchConfig(raw); const search = resolveLegacySearchConfig(raw);
if (!search) { if (!search) {
return []; return [];
@@ -143,7 +156,7 @@ export function listLegacyWebSearchConfigPaths(raw: unknown): string[] {
if ("apiKey" in search) { if ("apiKey" in search) {
paths.push("tools.web.search.apiKey"); paths.push("tools.web.search.apiKey");
} }
for (const providerId of getLegacyWebSearchProviderIds()) { for (const providerId of getLegacyWebSearchProviderIds(owners)) {
const scoped = search[providerId]; const scoped = search[providerId];
if (isRecord(scoped)) { if (isRecord(scoped)) {
for (const key of Object.keys(scoped)) { for (const key of Object.keys(scoped)) {
@@ -159,15 +172,17 @@ export function migrateLegacyWebSearchConfig<T>(raw: T): { config: T; changes: s
return { config: raw, changes: [] }; return { config: raw, changes: [] };
} }
if (!hasMappedLegacyWebSearchConfig(raw)) { const owners = getBundledLegacyWebSearchOwners();
if (!hasMappedLegacyWebSearchConfig(raw, owners)) {
return { config: raw, changes: [] }; return { config: raw, changes: [] };
} }
return normalizeLegacyWebSearchConfigRecord(raw); return normalizeLegacyWebSearchConfigRecord(raw, owners);
} }
function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>( function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
raw: T, raw: T,
owners: ReadonlyMap<string, string>,
): { ): {
config: T; config: T;
changes: string[]; changes: string[];
@@ -186,7 +201,7 @@ function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
if (key === "apiKey") { if (key === "apiKey") {
continue; continue;
} }
if (getLegacyWebSearchProviderIdSet().has(key) && isRecord(value)) { if (getLegacyWebSearchProviderIdSet(owners).has(key) && isRecord(value)) {
continue; continue;
} }
if (MODERN_SCOPED_WEB_SEARCH_KEYS.has(key) || !isRecord(value)) { if (MODERN_SCOPED_WEB_SEARCH_KEYS.has(key) || !isRecord(value)) {
@@ -195,7 +210,7 @@ function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
} }
web.search = nextSearch; web.search = nextSearch;
const globalSearchMigration = resolveLegacyGlobalWebSearchMigration(search); const globalSearchMigration = resolveLegacyGlobalWebSearchMigration(search, owners);
if (globalSearchMigration) { if (globalSearchMigration) {
migratePluginWebSearchConfig({ migratePluginWebSearchConfig({
root: nextRoot, root: nextRoot,
@@ -207,7 +222,7 @@ function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
}); });
} }
for (const providerId of getLegacyWebSearchProviderIds()) { for (const providerId of getLegacyWebSearchProviderIds(owners)) {
if (providerId === LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID) { if (providerId === LEGACY_GLOBAL_WEB_SEARCH_PROVIDER_ID) {
continue; continue;
} }
@@ -215,11 +230,7 @@ function normalizeLegacyWebSearchConfigRecord<T extends JsonRecord>(
if (!scoped || Object.keys(scoped).length === 0) { if (!scoped || Object.keys(scoped).length === 0) {
continue; continue;
} }
const pluginId = resolveManifestContractOwnerPluginId({ const pluginId = owners.get(providerId);
contract: "webSearchProviders",
value: providerId,
origin: "bundled",
});
if (!pluginId) { if (!pluginId) {
continue; continue;
} }

View File

@@ -4,7 +4,7 @@ const mocks = vi.hoisted(() => ({
installPluginFromNpmSpec: vi.fn(), installPluginFromNpmSpec: vi.fn(),
listChannelPluginCatalogEntries: vi.fn(), listChannelPluginCatalogEntries: vi.fn(),
loadInstalledPluginIndexInstallRecords: vi.fn(), loadInstalledPluginIndexInstallRecords: vi.fn(),
loadPluginManifestRegistryForPluginRegistry: vi.fn(), loadPluginMetadataSnapshot: vi.fn(),
resolveDefaultPluginExtensionsDir: vi.fn(() => "/tmp/openclaw-plugins"), resolveDefaultPluginExtensionsDir: vi.fn(() => "/tmp/openclaw-plugins"),
resolveProviderInstallCatalogEntries: vi.fn(), resolveProviderInstallCatalogEntries: vi.fn(),
updateNpmInstalledPlugins: vi.fn(), updateNpmInstalledPlugins: vi.fn(),
@@ -29,8 +29,8 @@ vi.mock("../../../plugins/install.js", () => ({
installPluginFromNpmSpec: mocks.installPluginFromNpmSpec, installPluginFromNpmSpec: mocks.installPluginFromNpmSpec,
})); }));
vi.mock("../../../plugins/plugin-registry.js", () => ({ vi.mock("../../../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry, loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
})); }));
vi.mock("../../../plugins/provider-install-catalog.js", () => ({ vi.mock("../../../plugins/provider-install-catalog.js", () => ({
@@ -44,7 +44,7 @@ vi.mock("../../../plugins/update.js", () => ({
describe("repairMissingConfiguredPluginInstalls", () => { describe("repairMissingConfiguredPluginInstalls", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({ mocks.loadPluginMetadataSnapshot.mockReturnValue({
plugins: [], plugins: [],
diagnostics: [], diagnostics: [],
}); });

View File

@@ -6,7 +6,7 @@ import { installPluginFromNpmSpec } from "../../../plugins/install.js";
import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js"; import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
import { writePersistedInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js"; import { writePersistedInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
import { buildNpmResolutionInstallFields } from "../../../plugins/installs.js"; import { buildNpmResolutionInstallFields } from "../../../plugins/installs.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.js";
import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js"; import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js";
import { updateNpmInstalledPlugins } from "../../../plugins/update.js"; import { updateNpmInstalledPlugins } from "../../../plugins/update.js";
import { asObjectRecord } from "./object.js"; import { asObjectRecord } from "./object.js";
@@ -153,12 +153,12 @@ export async function repairMissingConfiguredPluginInstalls(params: {
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
}): Promise<{ changes: string[]; warnings: string[] }> { }): Promise<{ changes: string[]; warnings: string[] }> {
const env = params.env ?? process.env; const env = params.env ?? process.env;
const registry = loadPluginManifestRegistryForPluginRegistry({ const knownIds = new Set(
config: params.cfg, loadPluginMetadataSnapshot({
env, config: params.cfg,
includeDisabled: true, env,
}); }).plugins.map((plugin) => plugin.id),
const knownIds = new Set(registry.plugins.map((plugin) => plugin.id)); );
const records = await loadInstalledPluginIndexInstallRecords({ env }); const records = await loadInstalledPluginIndexInstallRecords({ env });
const configuredPluginIds = collectConfiguredPluginIds(params.cfg); const configuredPluginIds = collectConfiguredPluginIds(params.cfg);
const missingRecordedPluginIds = Object.keys(records).filter( const missingRecordedPluginIds = Object.keys(records).filter(

View File

@@ -2,7 +2,7 @@ import { normalizeToolName } from "../../../agents/tool-policy-shared.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { normalizePluginId } from "../../../plugins/config-state.js"; import { normalizePluginId } from "../../../plugins/config-state.js";
import type { PluginManifestRegistry } from "../../../plugins/manifest-registry.js"; import type { PluginManifestRegistry } from "../../../plugins/manifest-registry.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.js";
type ToolAllowlistSource = { type ToolAllowlistSource = {
label: string; label: string;
@@ -147,11 +147,10 @@ export function collectPluginToolAllowlistWarnings(params: {
const registry = const registry =
params.manifestRegistry ?? params.manifestRegistry ??
loadPluginManifestRegistryForPluginRegistry({ loadPluginMetadataSnapshot({
config: params.cfg, config: params.cfg,
env: params.env, env: params.env ?? process.env,
includeDisabled: true, }).manifestRegistry;
});
const knownPluginIds = collectKnownPluginIds(registry); const knownPluginIds = collectKnownPluginIds(registry);
const toolOwners = collectToolOwners(registry); const toolOwners = collectToolOwners(registry);
const missingPluginIssues = new Map<string, Set<string>>(); const missingPluginIssues = new Map<string, Set<string>>();

View File

@@ -3,7 +3,7 @@ import { CHANNEL_IDS } from "../../../channels/ids.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { normalizePluginId } from "../../../plugins/config-state.js"; import { normalizePluginId } from "../../../plugins/config-state.js";
import { loadInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-records.js"; import { loadInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-records.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../../../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.js";
import { sanitizeForLog } from "../../../terminal/ansi.js"; import { sanitizeForLog } from "../../../terminal/ansi.js";
import { asObjectRecord } from "./object.js"; import { asObjectRecord } from "./object.js";
@@ -29,12 +29,11 @@ function collectPluginRegistryState(
env?: NodeJS.ProcessEnv, env?: NodeJS.ProcessEnv,
): StalePluginRegistryState { ): StalePluginRegistryState {
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)); const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
const registry = loadPluginManifestRegistryForPluginRegistry({ const registry = loadPluginMetadataSnapshot({
config: cfg, config: cfg,
workspaceDir: workspaceDir ?? undefined, workspaceDir: workspaceDir ?? undefined,
env, env: env ?? process.env,
includeDisabled: true, }).manifestRegistry;
});
const knownIds = new Set(registry.plugins.map((plugin) => plugin.id)); const knownIds = new Set(registry.plugins.map((plugin) => plugin.id));
const installedIds = new Set<string>(); const installedIds = new Set<string>();
for (const pluginId of Object.keys(cfg.plugins?.installs ?? {})) { for (const pluginId of Object.keys(cfg.plugins?.installs ?? {})) {

View File

@@ -1,22 +1,20 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({ const mocks = vi.hoisted(() => ({
loadPluginRegistrySnapshot: vi.fn(), loadPluginMetadataSnapshot: vi.fn(),
resolvePluginContributionOwners: vi.fn(), resolvePluginContributionOwners: vi.fn(),
getPluginRecord: vi.fn(), getPluginRecord: vi.fn(),
isPluginEnabled: vi.fn(), isPluginEnabled: vi.fn(),
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
})); }));
vi.mock("../../plugins/plugin-registry.js", () => ({ vi.mock("../../plugins/plugin-registry.js", () => ({
loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot,
resolvePluginContributionOwners: mocks.resolvePluginContributionOwners, resolvePluginContributionOwners: mocks.resolvePluginContributionOwners,
getPluginRecord: mocks.getPluginRecord, getPluginRecord: mocks.getPluginRecord,
isPluginEnabled: mocks.isPluginEnabled, isPluginEnabled: mocks.isPluginEnabled,
})); }));
vi.mock("../../plugins/manifest-registry-installed.js", () => ({ vi.mock("../../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistryForInstalledIndex, loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot,
})); }));
const moonshotPlugin = { const moonshotPlugin = {
@@ -53,10 +51,14 @@ describe("loadStaticManifestCatalogRowsForList", () => {
it("loads only static manifest catalog rows without a provider filter", async () => { it("loads only static manifest catalog rows without a provider filter", async () => {
const { loadStaticManifestCatalogRowsForList } = await import("./list.manifest-catalog.js"); const { loadStaticManifestCatalogRowsForList } = await import("./list.manifest-catalog.js");
const index = { plugins: [], diagnostics: [] }; const index = { plugins: [], diagnostics: [] };
mocks.loadPluginRegistrySnapshot.mockReturnValueOnce(index); const manifestRegistry = {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValueOnce({
plugins: [openrouterPlugin, moonshotPlugin], plugins: [openrouterPlugin, moonshotPlugin],
diagnostics: [], diagnostics: [],
};
mocks.loadPluginMetadataSnapshot.mockReturnValueOnce({
index,
manifestRegistry,
plugins: manifestRegistry.plugins,
}); });
expect( expect(
@@ -64,20 +66,23 @@ describe("loadStaticManifestCatalogRowsForList", () => {
cfg: {}, cfg: {},
}).map((row) => row.ref), }).map((row) => row.ref),
).toEqual(["moonshot/kimi-k2.6"]); ).toEqual(["moonshot/kimi-k2.6"]);
expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith({ expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({
index,
config: {}, config: {},
env: undefined, env: process.env,
}); });
}); });
it("loads refreshable manifest rows as registry-backed supplements", async () => { it("loads refreshable manifest rows as registry-backed supplements", async () => {
const { loadSupplementalManifestCatalogRowsForList } = const { loadSupplementalManifestCatalogRowsForList } =
await import("./list.manifest-catalog.js"); await import("./list.manifest-catalog.js");
mocks.loadPluginRegistrySnapshot.mockReturnValueOnce({ plugins: [], diagnostics: [] }); const manifestRegistry = {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValueOnce({
plugins: [openrouterPlugin, moonshotPlugin], plugins: [openrouterPlugin, moonshotPlugin],
diagnostics: [], diagnostics: [],
};
mocks.loadPluginMetadataSnapshot.mockReturnValueOnce({
index: { plugins: [], diagnostics: [] },
manifestRegistry,
plugins: manifestRegistry.plugins,
}); });
expect( expect(

View File

@@ -4,11 +4,11 @@ import {
planManifestModelCatalogRows, planManifestModelCatalogRows,
} from "../../model-catalog/index.js"; } from "../../model-catalog/index.js";
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js"; import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
import { loadPluginManifestRegistryForInstalledIndex } from "../../plugins/manifest-registry-installed.js"; import type { PluginManifestRegistry } from "../../plugins/manifest-registry.js";
import { loadPluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
import { import {
getPluginRecord, getPluginRecord,
isPluginEnabled, isPluginEnabled,
loadPluginRegistrySnapshot,
resolvePluginContributionOwners, resolvePluginContributionOwners,
type PluginRegistrySnapshot, type PluginRegistrySnapshot,
} from "../../plugins/plugin-registry.js"; } from "../../plugins/plugin-registry.js";
@@ -19,6 +19,7 @@ function loadManifestCatalogRowsForPluginIds(params: {
cfg: OpenClawConfig; cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
index: PluginRegistrySnapshot; index: PluginRegistrySnapshot;
registry: PluginManifestRegistry;
mode: ManifestCatalogRowsForListMode; mode: ManifestCatalogRowsForListMode;
pluginIds?: readonly string[]; pluginIds?: readonly string[];
providerFilter?: string; providerFilter?: string;
@@ -26,12 +27,13 @@ function loadManifestCatalogRowsForPluginIds(params: {
if (params.pluginIds && params.pluginIds.length === 0) { if (params.pluginIds && params.pluginIds.length === 0) {
return []; return [];
} }
const registry = loadPluginManifestRegistryForInstalledIndex({ const pluginIdSet = params.pluginIds ? new Set(params.pluginIds) : undefined;
index: params.index, const registry = pluginIdSet
config: params.cfg, ? {
env: params.env, ...params.registry,
pluginIds: params.pluginIds, plugins: params.registry.plugins.filter((plugin) => pluginIdSet.has(plugin.id)),
}); }
: params.registry;
const plan = planManifestModelCatalogRows({ const plan = planManifestModelCatalogRows({
registry, registry,
...(params.providerFilter ? { providerFilter: params.providerFilter } : {}), ...(params.providerFilter ? { providerFilter: params.providerFilter } : {}),
@@ -96,15 +98,17 @@ function loadManifestCatalogRowsForList(params: {
? normalizeModelCatalogProviderId(params.providerFilter) ? normalizeModelCatalogProviderId(params.providerFilter)
: undefined; : undefined;
const mode = params.mode ?? "static-authoritative"; const mode = params.mode ?? "static-authoritative";
const index = loadPluginRegistrySnapshot({ const snapshot = loadPluginMetadataSnapshot({
config: params.cfg, config: params.cfg,
env: params.env, env: params.env ?? process.env,
}); });
const index = snapshot.index;
if (!providerFilter) { if (!providerFilter) {
return loadManifestCatalogRowsForPluginIds({ return loadManifestCatalogRowsForPluginIds({
cfg: params.cfg, cfg: params.cfg,
env: params.env, env: params.env,
index, index,
registry: snapshot.manifestRegistry,
mode, mode,
}); });
} }
@@ -112,6 +116,7 @@ function loadManifestCatalogRowsForList(params: {
cfg: params.cfg, cfg: params.cfg,
env: params.env, env: params.env,
index, index,
registry: snapshot.manifestRegistry,
mode, mode,
pluginIds: resolveConventionModelCatalogPluginIds({ pluginIds: resolveConventionModelCatalogPluginIds({
cfg: params.cfg, cfg: params.cfg,
@@ -127,6 +132,7 @@ function loadManifestCatalogRowsForList(params: {
cfg: params.cfg, cfg: params.cfg,
env: params.env, env: params.env,
index, index,
registry: snapshot.manifestRegistry,
mode, mode,
pluginIds: resolveDeclaredModelCatalogPluginIds({ pluginIds: resolveDeclaredModelCatalogPluginIds({
cfg: params.cfg, cfg: params.cfg,

View File

@@ -3,11 +3,11 @@ import path from "node:path";
import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js"; import { createSubsystemLogger } from "../logging/subsystem.js";
import { import {
normalizePluginsConfig, normalizePluginsConfigWithResolver,
resolveEffectivePluginActivationState, resolveEffectivePluginActivationState,
resolveMemorySlotDecision, resolveMemorySlotDecision,
} from "../plugins/config-state.js"; } from "../plugins/config-policy.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import { hasKind } from "../plugins/slots.js"; import { hasKind } from "../plugins/slots.js";
import { isPathInsideWithRealpath } from "../security/scan-paths.js"; import { isPathInsideWithRealpath } from "../security/scan-paths.js";
@@ -26,17 +26,20 @@ export function resolvePluginHookDirs(params: {
if (!workspaceDir) { if (!workspaceDir) {
return []; return [];
} }
const registry = loadPluginManifestRegistryForPluginRegistry({ const metadataSnapshot = loadPluginMetadataSnapshot({
workspaceDir, workspaceDir,
config: params.config, config: params.config ?? {},
// Hook discovery should reflect freshly written bundle manifests immediately. env: process.env,
includeDisabled: true,
}); });
const registry = metadataSnapshot.manifestRegistry;
if (registry.plugins.length === 0) { if (registry.plugins.length === 0) {
return []; return [];
} }
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins); const normalizedPlugins = normalizePluginsConfigWithResolver(
params.config?.plugins,
metadataSnapshot.normalizePluginId,
);
const memorySlot = normalizedPlugins.slots.memory; const memorySlot = normalizedPlugins.slots.memory;
let selectedMemoryPluginId: string | null = null; let selectedMemoryPluginId: string | null = null;
const seen = new Set<string>(); const seen = new Set<string>();

View File

@@ -22,6 +22,16 @@ vi.mock("../plugins/plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry: loadPluginManifestRegistry, loadPluginManifestRegistryForPluginRegistry: loadPluginManifestRegistry,
})); }));
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginMetadataSnapshot: () => {
const registry = loadPluginManifestRegistry();
return {
plugins: registry.plugins,
manifestRegistry: registry,
};
},
}));
import { buildTrajectoryArtifacts, buildTrajectoryRunMetadata } from "./metadata.js"; import { buildTrajectoryArtifacts, buildTrajectoryRunMetadata } from "./metadata.js";
afterEach(() => { afterEach(() => {

View File

@@ -10,7 +10,7 @@ import {
sanitizeSupportSnapshotValue, sanitizeSupportSnapshotValue,
type SupportRedactionContext, type SupportRedactionContext,
} from "../logging/diagnostic-support-redaction.js"; } from "../logging/diagnostic-support-redaction.js";
import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import { getActivePluginRegistry, listImportedRuntimePluginIds } from "../plugins/runtime.js"; import { getActivePluginRegistry, listImportedRuntimePluginIds } from "../plugins/runtime.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
@@ -136,15 +136,14 @@ function buildPluginsFromManifest(params: {
workspaceDir?: string; workspaceDir?: string;
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
}) { }) {
const registry = loadPluginManifestRegistryForPluginRegistry({ const snapshot = loadPluginMetadataSnapshot({
config: params.config, config: params.config ?? {},
workspaceDir: params.workspaceDir, workspaceDir: params.workspaceDir,
env: params.env, env: params.env ?? process.env,
includeDisabled: true,
}); });
return { return {
source: "manifest-registry", source: "manifest-registry",
entries: registry.plugins entries: snapshot.plugins
.map((plugin) => ({ .map((plugin) => ({
id: plugin.id, id: plugin.id,
name: plugin.name, name: plugin.name,

View File

@@ -18,6 +18,16 @@ vi.mock("../plugins/plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry: loadPluginManifestRegistry, loadPluginManifestRegistryForPluginRegistry: loadPluginManifestRegistry,
})); }));
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
loadPluginMetadataSnapshot: () => {
const registry = loadPluginManifestRegistry();
return {
plugins: registry.plugins,
manifestRegistry: registry,
};
},
}));
function makeManifestPlugin( function makeManifestPlugin(
id: string, id: string,
uiHints?: Record<string, PluginConfigUiHint>, uiHints?: Record<string, PluginConfigUiHint>,

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import type { PluginConfigUiHint } from "../plugins/types.js"; import type { PluginConfigUiHint } from "../plugins/types.js";
import { getPath, setPathCreateStrict } from "../secrets/path-utils.js"; import { getPath, setPathCreateStrict } from "../secrets/path-utils.js";
import type { JsonSchemaObject } from "../shared/json-schema.types.js"; import type { JsonSchemaObject } from "../shared/json-schema.types.js";
@@ -16,13 +17,13 @@ export type ConfigurablePlugin = {
jsonSchema?: JsonSchemaObject; jsonSchema?: JsonSchemaObject;
}; };
type PluginRegistryModule = typeof import("../plugins/plugin-registry.js"); type PluginMetadataSnapshotModule = typeof import("../plugins/plugin-metadata-snapshot.js");
let pluginRegistryModulePromise: Promise<PluginRegistryModule> | undefined; let pluginMetadataSnapshotModulePromise: Promise<PluginMetadataSnapshotModule> | undefined;
function loadPluginRegistryModule(): Promise<PluginRegistryModule> { function loadPluginMetadataSnapshotModule(): Promise<PluginMetadataSnapshotModule> {
pluginRegistryModulePromise ??= import("../plugins/plugin-registry.js"); pluginMetadataSnapshotModulePromise ??= import("../plugins/plugin-metadata-snapshot.js");
return pluginRegistryModulePromise; return pluginMetadataSnapshotModulePromise;
} }
type JsonSchemaProperty = { type JsonSchemaProperty = {
@@ -141,6 +142,22 @@ export function discoverUnconfiguredPlugins(params: {
}); });
} }
async function listEnabledConfigurableManifestPlugins(params: {
config: OpenClawConfig;
workspaceDir?: string;
}): Promise<readonly PluginManifestRecord[]> {
const { loadPluginMetadataSnapshot } = await loadPluginMetadataSnapshotModule();
const snapshot = loadPluginMetadataSnapshot({
config: params.config,
workspaceDir: params.workspaceDir,
env: process.env,
});
return snapshot.plugins.filter((plugin) => {
const entry = params.config.plugins?.entries?.[plugin.id];
return plugin.enabledByDefault || entry?.enabled === true;
});
}
/** /**
* Prompt the user to configure a single plugin's fields via uiHints. * Prompt the user to configure a single plugin's fields via uiHints.
* Returns the updated config with plugin values applied. * Returns the updated config with plugin values applied.
@@ -299,20 +316,13 @@ export async function setupPluginConfig(params: {
prompter: WizardPrompter; prompter: WizardPrompter;
workspaceDir?: string; workspaceDir?: string;
}): Promise<OpenClawConfig> { }): Promise<OpenClawConfig> {
const { loadPluginManifestRegistryForPluginRegistry } = await loadPluginRegistryModule(); const manifestPlugins = await listEnabledConfigurableManifestPlugins({
const registry = loadPluginManifestRegistryForPluginRegistry({
config: params.config, config: params.config,
workspaceDir: params.workspaceDir, workspaceDir: params.workspaceDir,
includeDisabled: true,
}); });
const unconfigured = discoverUnconfiguredPlugins({ const unconfigured = discoverUnconfiguredPlugins({
manifestPlugins: registry.plugins.filter((p) => { manifestPlugins,
// Only show enabled plugins
const entry = params.config.plugins?.entries?.[p.id];
// Plugin is discoverable if it's enabled or enabledByDefault and not denied
return p.enabledByDefault || entry?.enabled === true;
}),
config: params.config, config: params.config,
}); });
@@ -362,18 +372,13 @@ export async function configurePluginConfig(params: {
prompter: WizardPrompter; prompter: WizardPrompter;
workspaceDir?: string; workspaceDir?: string;
}): Promise<OpenClawConfig> { }): Promise<OpenClawConfig> {
const { loadPluginManifestRegistryForPluginRegistry } = await loadPluginRegistryModule(); const manifestPlugins = await listEnabledConfigurableManifestPlugins({
const registry = loadPluginManifestRegistryForPluginRegistry({
config: params.config, config: params.config,
workspaceDir: params.workspaceDir, workspaceDir: params.workspaceDir,
includeDisabled: true,
}); });
const configurable = discoverConfigurablePlugins({ const configurable = discoverConfigurablePlugins({
manifestPlugins: registry.plugins.filter((p) => { manifestPlugins,
const entry = params.config.plugins?.entries?.[p.id];
return p.enabledByDefault || entry?.enabled === true;
}),
}); });
if (configurable.length === 0) { if (configurable.length === 0) {