mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
refactor: route plugin metadata consumers through snapshots
This commit is contained in:
@@ -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: [] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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>>();
|
||||||
|
|||||||
@@ -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 ?? {})) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user