diff --git a/src/gateway/model-pricing-cache.test.ts b/src/gateway/model-pricing-cache.test.ts index 9f9890c2d4d..3010e71181e 100644 --- a/src/gateway/model-pricing-cache.test.ts +++ b/src/gateway/model-pricing-cache.test.ts @@ -202,6 +202,77 @@ describe("model-pricing-cache", () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it("uses a provided metadata registry view without rebuilding manifest metadata", async () => { + const manifestRegistry = { + diagnostics: [], + plugins: [ + createManifestRecord({ + id: "search-plugin", + contracts: { webSearchProviders: ["search-plugin"] }, + }), + ], + }; + const config = { + plugins: { + entries: { + "search-plugin": { + config: { + webSearch: { + model: "local-search/search-model", + }, + }, + }, + }, + }, + models: { + providers: { + "local-search": { + baseUrl: "http://127.0.0.1:43210/v1", + api: "openai-completions", + models: [ + { + id: "search-model", + cost: { input: 0.2, output: 0.4 }, + }, + ], + }, + }, + }, + } as unknown as OpenClawConfig; + const fetchImpl = vi.fn(); + + await refreshGatewayModelPricingCache({ + config, + fetchImpl, + pluginMetadataSnapshot: { + index: { + plugins: [ + { + pluginId: "search-plugin", + origin: "global", + enabled: true, + enabledByDefault: true, + }, + ], + } as never, + manifestRegistry, + }, + }); + + expect( + pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex, + ).not.toHaveBeenCalled(); + expect(fetchImpl).not.toHaveBeenCalled(); + expect( + getCachedGatewayModelPricing({ provider: "local-search", model: "search-model" }), + ).toEqual({ + input: 0.2, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }); + }); + it("does not load plugin manifests for pricing when plugins are globally disabled", async () => { const config = { plugins: { diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index d8d3756d27e..6dafd6a90be 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -13,18 +13,15 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { planManifestModelCatalogRows, type ModelCatalogCost } from "../model-catalog/index.js"; import { isInstalledPluginEnabled } from "../plugins/installed-plugin-index.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js"; import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import type { PluginManifestModelPricingModelIdTransform, PluginManifestModelPricingProvider, PluginManifestModelPricingSource, } from "../plugins/manifest.js"; -import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js"; -import { - loadPluginRegistrySnapshot, - type PluginRegistrySnapshot, -} from "../plugins/plugin-registry.js"; +import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; +import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js"; +import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js"; import { normalizeOptionalString, resolvePrimaryStringValue } from "../shared/string-coerce.js"; import { clearGatewayModelPricingCacheState, @@ -400,15 +397,19 @@ function filterActiveManifestRegistry(params: { function resolveModelPricingManifestMetadata(params: { config: OpenClawConfig; - pluginLookUpTable?: Pick; + env?: NodeJS.ProcessEnv; + workspaceDir?: string; + pluginMetadataSnapshot?: PluginMetadataRegistryView; + pluginLookUpTable?: PluginMetadataRegistryView; manifestRegistry?: PluginManifestRegistry; }): ModelPricingManifestMetadata { - if (params.pluginLookUpTable) { + const metadataSnapshot = params.pluginMetadataSnapshot ?? params.pluginLookUpTable; + if (metadataSnapshot) { return { - allRegistry: params.pluginLookUpTable.manifestRegistry, + allRegistry: metadataSnapshot.manifestRegistry, activeRegistry: filterActiveManifestRegistry({ - registry: params.pluginLookUpTable.manifestRegistry, - index: params.pluginLookUpTable.index, + registry: metadataSnapshot.manifestRegistry, + index: metadataSnapshot.index, config: params.config, }), }; @@ -426,17 +427,16 @@ function resolveModelPricingManifestMetadata(params: { activeRegistry: emptyRegistry, }; } - const index = loadPluginRegistrySnapshot({ config: params.config }); - const allRegistry = loadPluginManifestRegistryForInstalledIndex({ - index, + const snapshot = loadPluginMetadataSnapshot({ config: params.config, - includeDisabled: true, + env: params.env ?? process.env, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); return { - allRegistry, + allRegistry: snapshot.manifestRegistry, activeRegistry: filterActiveManifestRegistry({ - registry: allRegistry, - index, + registry: snapshot.manifestRegistry, + index: snapshot.index, config: params.config, }), }; @@ -1102,8 +1102,11 @@ function collectSeededPricing(params: { export async function refreshGatewayModelPricingCache(params: { config: OpenClawConfig; + env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; - pluginLookUpTable?: Pick; + workspaceDir?: string; + pluginMetadataSnapshot?: PluginMetadataRegistryView; + pluginLookUpTable?: PluginMetadataRegistryView; manifestRegistry?: PluginManifestRegistry; }): Promise { if (!isGatewayModelPricingEnabled(params.config)) { @@ -1117,6 +1120,9 @@ export async function refreshGatewayModelPricingCache(params: { inFlightRefresh = (async () => { const manifestMetadata = resolveModelPricingManifestMetadata({ config: params.config, + env: params.env, + workspaceDir: params.workspaceDir, + pluginMetadataSnapshot: params.pluginMetadataSnapshot, pluginLookUpTable: params.pluginLookUpTable, manifestRegistry: params.manifestRegistry, }); @@ -1251,8 +1257,11 @@ export async function refreshGatewayModelPricingCache(params: { export function startGatewayModelPricingRefresh(params: { config: OpenClawConfig; + env?: NodeJS.ProcessEnv; fetchImpl?: typeof fetch; - pluginLookUpTable?: Pick; + workspaceDir?: string; + pluginMetadataSnapshot?: PluginMetadataRegistryView; + pluginLookUpTable?: PluginMetadataRegistryView; manifestRegistry?: PluginManifestRegistry; }): () => void { if (!isGatewayModelPricingEnabled(params.config)) { diff --git a/src/gateway/server-runtime-services.ts b/src/gateway/server-runtime-services.ts index 1e388304b7d..84d943dd80f 100644 --- a/src/gateway/server-runtime-services.ts +++ b/src/gateway/server-runtime-services.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isVitestRuntimeEnv } from "../infra/env.js"; import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js"; -import type { PluginLookUpTable } from "../plugins/plugin-lookup-table.js"; +import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js"; import type { ChannelHealthMonitor } from "./channel-health-monitor.js"; import { startChannelHealthMonitor } from "./channel-health-monitor.js"; import { isGatewayModelPricingEnabled } from "./model-pricing-config.js"; @@ -91,7 +91,7 @@ function recoverPendingSessionDeliveries(params: { function startGatewayModelPricingRefreshOnDemand(params: { config: OpenClawConfig; - pluginLookUpTable?: Pick; + pluginLookUpTable?: PluginMetadataRegistryView; log: GatewayRuntimeServiceLogger; }): () => void { if (!isGatewayModelPricingEnabled(params.config)) { @@ -125,7 +125,7 @@ export function startGatewayRuntimeServices(params: { cfgAtStart: OpenClawConfig; channelManager: GatewayChannelManager; log: GatewayRuntimeServiceLogger; - pluginLookUpTable?: Pick; + pluginLookUpTable?: PluginMetadataRegistryView; }): { heartbeatRunner: HeartbeatRunner; channelHealthMonitor: ChannelHealthMonitor | null; diff --git a/src/plugins/manifest-contract-eligibility.test.ts b/src/plugins/manifest-contract-eligibility.test.ts new file mode 100644 index 00000000000..f030f0725d7 --- /dev/null +++ b/src/plugins/manifest-contract-eligibility.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getCurrentPluginMetadataSnapshot: vi.fn(), + loadPluginMetadataSnapshot: vi.fn(), +})); + +vi.mock("./current-plugin-metadata-snapshot.js", () => ({ + getCurrentPluginMetadataSnapshot: mocks.getCurrentPluginMetadataSnapshot, +})); + +vi.mock("./plugin-metadata-snapshot.js", () => ({ + loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot, +})); + +import { loadManifestContractSnapshot } from "./manifest-contract-eligibility.js"; + +describe("loadManifestContractSnapshot", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(undefined); + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: { plugins: [] }, + plugins: [], + }); + }); + + it("checks the current metadata snapshot with env and workspace scope", () => { + const env = { HOME: "/home/snapshot" } as NodeJS.ProcessEnv; + const current = { + index: { plugins: [] }, + plugins: [], + }; + mocks.getCurrentPluginMetadataSnapshot.mockReturnValue(current); + + expect(loadManifestContractSnapshot({ config: {}, workspaceDir: "/workspace", env })).toBe( + current, + ); + + expect(mocks.getCurrentPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: {}, + env, + workspaceDir: "/workspace", + }); + expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); + }); + + it("falls back to the shared metadata snapshot loader", () => { + const env = { HOME: "/home/fallback" } as NodeJS.ProcessEnv; + const snapshot = { + index: { plugins: [{ pluginId: "demo" }] }, + plugins: [{ id: "demo" }], + }; + mocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot); + + expect(loadManifestContractSnapshot({ config: {}, env })).toEqual({ + index: snapshot.index, + plugins: snapshot.plugins, + }); + + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: {}, + env, + }); + }); +}); diff --git a/src/plugins/manifest-contract-eligibility.ts b/src/plugins/manifest-contract-eligibility.ts index a32f63f82be..c7a0c3ce2ca 100644 --- a/src/plugins/manifest-contract-eligibility.ts +++ b/src/plugins/manifest-contract-eligibility.ts @@ -1,10 +1,12 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js"; import { isInstalledPluginEnabled } from "./installed-plugin-index.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; import type { PluginManifestContractListKey, PluginManifestRecord } from "./manifest-registry.js"; -import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.types.js"; -import { loadPluginRegistrySnapshot } from "./plugin-registry.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; +import type { + PluginMetadataManifestView, + PluginMetadataSnapshot, +} from "./plugin-metadata-snapshot.types.js"; export function isManifestPluginAvailableForControlPlane(params: { snapshot: Pick; @@ -65,28 +67,23 @@ export function loadManifestContractSnapshot(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; -}): Pick { +}): PluginMetadataManifestView { + const env = params.env ?? process.env; const current = getCurrentPluginMetadataSnapshot({ config: params.config, + env, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); if (current) { return current; } - const env = params.env ?? process.env; - const index = loadPluginRegistrySnapshot({ - config: params.config, + const snapshot = loadPluginMetadataSnapshot({ + config: params.config ?? {}, env, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); return { - index, - plugins: loadPluginManifestRegistryForInstalledIndex({ - index, - config: params.config, - env, - includeDisabled: true, - ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), - }).plugins, + index: snapshot.index, + plugins: snapshot.plugins, }; } diff --git a/src/plugins/plugin-metadata-snapshot.ts b/src/plugins/plugin-metadata-snapshot.ts index 124f13a80f8..6e003a00fc6 100644 --- a/src/plugins/plugin-metadata-snapshot.ts +++ b/src/plugins/plugin-metadata-snapshot.ts @@ -17,6 +17,8 @@ import { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalize import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry-snapshot.js"; export type { LoadPluginMetadataSnapshotParams, + PluginMetadataManifestView, + PluginMetadataRegistryView, PluginMetadataSnapshot, PluginMetadataSnapshotMetrics, PluginMetadataSnapshotOwnerMaps, @@ -146,6 +148,12 @@ function buildPluginMetadataOwnerMaps( }; } +export function listPluginOriginsFromMetadataSnapshot( + snapshot: Pick, +): ReadonlyMap { + return new Map(snapshot.plugins.map((record) => [record.id, record.origin])); +} + export function loadPluginMetadataSnapshot( params: LoadPluginMetadataSnapshotParams, ): PluginMetadataSnapshot { diff --git a/src/plugins/plugin-metadata-snapshot.types.ts b/src/plugins/plugin-metadata-snapshot.types.ts index 10eacc1afdc..90335a01961 100644 --- a/src/plugins/plugin-metadata-snapshot.types.ts +++ b/src/plugins/plugin-metadata-snapshot.types.ts @@ -48,6 +48,10 @@ export type PluginMetadataSnapshot = { metrics: PluginMetadataSnapshotMetrics; }; +export type PluginMetadataRegistryView = Pick; + +export type PluginMetadataManifestView = Pick; + export type LoadPluginMetadataSnapshotParams = { config: OpenClawConfig; workspaceDir?: string; diff --git a/src/plugins/provider-discovery.runtime.test.ts b/src/plugins/provider-discovery.runtime.test.ts index 3a1114801ed..11bbb08a711 100644 --- a/src/plugins/provider-discovery.runtime.test.ts +++ b/src/plugins/provider-discovery.runtime.test.ts @@ -3,20 +3,19 @@ import type { PluginManifestRecord } from "./manifest-registry.js"; import type { ProviderPlugin } from "./types.js"; const mocks = vi.hoisted(() => ({ - loadPluginRegistrySnapshot: vi.fn(), - loadPluginManifestRegistryForInstalledIndex: vi.fn(), + loadPluginMetadataSnapshot: vi.fn(), resolveDiscoveredProviderPluginIds: vi.fn(), resolvePluginProviders: vi.fn(), loadSource: vi.fn(), })); -vi.mock("./plugin-registry.js", () => ({ - loadPluginRegistrySnapshot: mocks.loadPluginRegistrySnapshot, -})); - -vi.mock("./manifest-registry-installed.js", () => ({ - loadPluginManifestRegistryForInstalledIndex: mocks.loadPluginManifestRegistryForInstalledIndex, -})); +vi.mock("./plugin-metadata-snapshot.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadPluginMetadataSnapshot: mocks.loadPluginMetadataSnapshot, + }; +}); vi.mock("./providers.js", () => ({ resolveDiscoveredProviderPluginIds: mocks.resolveDiscoveredProviderPluginIds, @@ -82,11 +81,13 @@ function createProvider(params: { id: string; mode: "static" | "catalog" }): Pro describe("resolvePluginDiscoveryProvidersRuntime", () => { beforeEach(() => { vi.clearAllMocks(); - mocks.loadPluginRegistrySnapshot.mockReturnValue({ plugins: [] }); mocks.resolveDiscoveredProviderPluginIds.mockReturnValue(["deepseek"]); - mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ - plugins: [createManifestPlugin("deepseek")], - diagnostics: [], + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: { plugins: [] }, + manifestRegistry: { + plugins: [createManifestPlugin("deepseek")], + diagnostics: [], + }, }); }); @@ -116,20 +117,23 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { "kilocode", "unused", ]); - mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ - plugins: [ - createManifestPlugin("codex"), - createManifestPlugin("deepseek"), - createManifestPluginWithoutDiscovery({ - id: "kilocode", - providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] }, - }), - createManifestPluginWithoutDiscovery({ - id: "unused", - providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] }, - }), - ], - diagnostics: [], + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: { plugins: [] }, + manifestRegistry: { + plugins: [ + createManifestPlugin("codex"), + createManifestPlugin("deepseek"), + createManifestPluginWithoutDiscovery({ + id: "kilocode", + providerAuthEnvVars: { kilocode: ["KILOCODE_API_KEY"] }, + }), + createManifestPluginWithoutDiscovery({ + id: "unused", + providerAuthEnvVars: { unused: ["UNUSED_API_KEY"] }, + }), + ], + diagnostics: [], + }, }); mocks.loadSource.mockImplementation((modulePath: string) => modulePath.includes("/codex/") @@ -150,27 +154,25 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { ); }); - it("shares one registry snapshot and manifest registry between provider id discovery and entry loading", () => { + it("shares one metadata snapshot between provider id discovery and entry loading", () => { const registry = { plugins: [] }; const manifestRegistry = { plugins: [createManifestPlugin("deepseek")], diagnostics: [], }; - mocks.loadPluginRegistrySnapshot.mockReturnValue(registry); - mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: registry, + manifestRegistry, + }); mocks.loadSource.mockReturnValue(createProvider({ id: "deepseek", mode: "catalog" })); resolvePluginDiscoveryProvidersRuntime({ config: {}, env: {} as NodeJS.ProcessEnv }); - expect(mocks.loadPluginRegistrySnapshot).toHaveBeenCalledOnce(); - expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith({ - index: registry, + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({ config: {}, - workspaceDir: undefined, env: {}, - includeDisabled: true, }); - expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); + expect(mocks.loadPluginMetadataSnapshot).toHaveBeenCalledOnce(); expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith( expect.objectContaining({ registry, @@ -203,8 +205,7 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { }), ]); - expect(mocks.loadPluginRegistrySnapshot).not.toHaveBeenCalled(); - expect(mocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); + expect(mocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); expect(mocks.resolveDiscoveredProviderPluginIds).toHaveBeenCalledWith( expect.objectContaining({ registry, @@ -228,9 +229,12 @@ describe("resolvePluginDiscoveryProvidersRuntime", () => { }); it("does not fall back to full plugin loading when discovery entries are requested only", () => { - mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ - plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })], - diagnostics: [], + mocks.loadPluginMetadataSnapshot.mockReturnValue({ + index: { plugins: [] }, + manifestRegistry: { + plugins: [createManifestPluginWithoutDiscovery({ id: "deepseek" })], + diagnostics: [], + }, }); expect(resolvePluginDiscoveryProvidersRuntime({ discoveryEntriesOnly: true })).toEqual([]); diff --git a/src/plugins/provider-discovery.runtime.ts b/src/plugins/provider-discovery.runtime.ts index 716ef2a82dc..a2bcf157bcf 100644 --- a/src/plugins/provider-discovery.runtime.ts +++ b/src/plugins/provider-discovery.runtime.ts @@ -1,8 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; -import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; -import { loadPluginRegistrySnapshot } from "./plugin-registry.js"; +import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; +import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js"; import { resolveDiscoveredProviderPluginIds } from "./providers.js"; import { resolvePluginProviders } from "./providers.runtime.js"; import { createPluginSourceLoader } from "./source-loader.js"; @@ -77,18 +76,17 @@ function resolveProviderDiscoveryEntryPlugins(params: { includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; discoveryEntriesOnly?: boolean; - pluginMetadataSnapshot?: Pick; + pluginMetadataSnapshot?: PluginMetadataRegistryView; }): ProviderDiscoveryEntryResult { - const registry = params.pluginMetadataSnapshot?.index ?? loadPluginRegistrySnapshot(params); - const manifestRegistry = - params.pluginMetadataSnapshot?.manifestRegistry ?? - loadPluginManifestRegistryForInstalledIndex({ - index: registry, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - includeDisabled: true, + const metadataSnapshot = + params.pluginMetadataSnapshot ?? + loadPluginMetadataSnapshot({ + config: params.config ?? {}, + env: params.env ?? process.env, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }); + const registry = metadataSnapshot.index; + const manifestRegistry = metadataSnapshot.manifestRegistry; const pluginIds = resolveDiscoveredProviderPluginIds({ ...params, registry, @@ -148,10 +146,10 @@ export function resolvePluginDiscoveryProvidersRuntime(params: { includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; discoveryEntriesOnly?: boolean; - pluginMetadataSnapshot?: Pick; + pluginMetadataSnapshot?: PluginMetadataRegistryView; }): ProviderPlugin[] { const env = params.env ?? process.env; - const entryResult = resolveProviderDiscoveryEntryPlugins(params); + const entryResult = resolveProviderDiscoveryEntryPlugins({ ...params, env }); if (params.discoveryEntriesOnly === true) { return entryResult.providers; } diff --git a/src/plugins/provider-discovery.ts b/src/plugins/provider-discovery.ts index 6014805b5dd..5e3f9eea30a 100644 --- a/src/plugins/provider-discovery.ts +++ b/src/plugins/provider-discovery.ts @@ -1,7 +1,7 @@ import { normalizeProviderId } from "../agents/model-selection.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js"; +import type { PluginMetadataRegistryView } from "./plugin-metadata-snapshot.types.js"; import { listPluginContributionIds, loadPluginRegistrySnapshot, @@ -43,7 +43,7 @@ export type ResolveRuntimePluginDiscoveryProvidersParams = { includeUntrustedWorkspacePlugins?: boolean; requireCompleteDiscoveryEntryCoverage?: boolean; discoveryEntriesOnly?: boolean; - pluginMetadataSnapshot?: Pick; + pluginMetadataSnapshot?: PluginMetadataRegistryView; }; export type ResolveInstalledPluginProviderContributionIdsParams = LoadPluginRegistryParams & { diff --git a/src/secrets/runtime-manifest.runtime.ts b/src/secrets/runtime-manifest.runtime.ts index e77673b1a3c..e55e8b2b322 100644 --- a/src/secrets/runtime-manifest.runtime.ts +++ b/src/secrets/runtime-manifest.runtime.ts @@ -1,2 +1,4 @@ -export { loadPluginManifestRegistryForInstalledIndex } from "../plugins/manifest-registry-installed.js"; -export { loadPluginRegistrySnapshot } from "../plugins/plugin-registry.js"; +export { + listPluginOriginsFromMetadataSnapshot, + loadPluginMetadataSnapshot, +} from "../plugins/plugin-metadata-snapshot.js"; diff --git a/src/secrets/runtime.loadable-plugin-origins.test.ts b/src/secrets/runtime.loadable-plugin-origins.test.ts index e854c39291b..4e0e9d4314c 100644 --- a/src/secrets/runtime.loadable-plugin-origins.test.ts +++ b/src/secrets/runtime.loadable-plugin-origins.test.ts @@ -2,25 +2,32 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts"; const manifestMocks = vi.hoisted(() => ({ - loadPluginManifestRegistryForInstalledIndex: vi.fn(), - loadPluginRegistrySnapshot: vi.fn(() => ({ plugins: [] })), + listPluginOriginsFromMetadataSnapshot: vi.fn( + (snapshot: { plugins: Array<{ id: string; origin: string }> }) => + new Map(snapshot.plugins.map((record) => [record.id, record.origin])), + ), + loadPluginMetadataSnapshot: vi.fn<() => { plugins: Array<{ id: string; origin: string }> }>( + () => ({ + plugins: [], + }), + ), })); vi.mock("./runtime-manifest.runtime.js", () => ({ - loadPluginManifestRegistryForInstalledIndex: - manifestMocks.loadPluginManifestRegistryForInstalledIndex, - loadPluginRegistrySnapshot: manifestMocks.loadPluginRegistrySnapshot, + listPluginOriginsFromMetadataSnapshot: manifestMocks.listPluginOriginsFromMetadataSnapshot, + loadPluginMetadataSnapshot: manifestMocks.loadPluginMetadataSnapshot, })); const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks(); describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => { afterEach(() => { - manifestMocks.loadPluginManifestRegistryForInstalledIndex.mockReset(); - manifestMocks.loadPluginRegistrySnapshot.mockReset(); + manifestMocks.listPluginOriginsFromMetadataSnapshot.mockClear(); + manifestMocks.loadPluginMetadataSnapshot.mockReset(); + manifestMocks.loadPluginMetadataSnapshot.mockReturnValue({ plugins: [] }); }); - it("skips manifest registry loading when plugin entries are absent", async () => { + it("skips metadata snapshot loading when plugin entries are absent", async () => { await prepareSecretsRuntimeSnapshot({ config: asConfig({ models: { @@ -36,7 +43,42 @@ describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => { includeAuthStoreRefs: false, }); - expect(manifestMocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); - expect(manifestMocks.loadPluginRegistrySnapshot).not.toHaveBeenCalled(); + expect(manifestMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled(); + expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).not.toHaveBeenCalled(); + }); + + it("derives loadable plugin origins from the shared metadata snapshot", async () => { + const snapshot = { + plugins: [{ id: "demo", origin: "workspace" }], + }; + manifestMocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot); + + await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + plugins: { + entries: { + demo: { + config: { + apiKey: { source: "env", provider: "default", id: "DEMO_API_KEY" }, + }, + }, + }, + }, + }), + env: { HOME: "/home/demo", DEMO_API_KEY: "sk-demo" }, + includeAuthStoreRefs: false, + }); + + expect(manifestMocks.loadPluginMetadataSnapshot).toHaveBeenCalledWith({ + config: expect.objectContaining({ + plugins: expect.any(Object), + }), + workspaceDir: expect.any(String), + env: expect.objectContaining({ + HOME: "/home/demo", + DEMO_API_KEY: "sk-demo", + }), + }); + expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).toHaveBeenCalledWith(snapshot); }); }); diff --git a/src/secrets/runtime.ts b/src/secrets/runtime.ts index c07cdb27ebd..cf572a4d534 100644 --- a/src/secrets/runtime.ts +++ b/src/secrets/runtime.ts @@ -140,21 +140,14 @@ async function resolveLoadablePluginOrigins(params: { params.config, resolveDefaultAgentId(params.config), ); - const { loadPluginManifestRegistryForInstalledIndex, loadPluginRegistrySnapshot } = + const { listPluginOriginsFromMetadataSnapshot, loadPluginMetadataSnapshot } = await loadRuntimeManifestHelpers(); - const index = loadPluginRegistrySnapshot({ + const snapshot = loadPluginMetadataSnapshot({ config: params.config, workspaceDir, env: params.env, }); - const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({ - index, - config: params.config, - workspaceDir, - env: params.env, - includeDisabled: true, - }); - return new Map(manifestRegistry.plugins.map((record) => [record.id, record.origin])); + return listPluginOriginsFromMetadataSnapshot(snapshot); } function mergeSecretsRuntimeEnv(