diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index dc33d759295..a5d23b3467f 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -48,11 +48,6 @@ const InstalledPluginIndexStartupSchema = z const InstalledPluginIndexRecordSchema = z .object({ pluginId: z.string(), - contributionMetadataVersion: z.number().optional(), - name: z.string().optional(), - description: z.string().optional(), - manifestVersion: z.string().optional(), - legacyPluginIds: StringArraySchema.optional(), packageName: z.string().optional(), packageVersion: z.string().optional(), installRecord: z.record(z.string(), z.unknown()).optional(), @@ -63,16 +58,6 @@ const InstalledPluginIndexRecordSchema = z manifestHash: z.string(), format: z.string().optional(), bundleFormat: z.string().optional(), - bundleCapabilities: StringArraySchema.optional(), - kind: z.unknown().optional(), - channels: StringArraySchema.optional(), - providers: StringArraySchema.optional(), - cliBackends: StringArraySchema.optional(), - setupProviders: StringArraySchema.optional(), - channelConfigs: StringArraySchema.optional(), - modelCatalogProviders: StringArraySchema.optional(), - commandAliases: StringArraySchema.optional(), - contractKeys: StringArraySchema.optional(), source: z.string().optional(), setupSource: z.string().optional(), packageJson: z diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 798c3bfb2b8..14d06879758 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -175,18 +175,8 @@ describe("installed plugin index", () => { plugins: [ { pluginId: "demo", - contributionMetadataVersion: 1, - name: "Demo", packageName: "@vendor/demo-plugin", packageVersion: "1.2.3", - channels: ["demo-chat"], - channelConfigs: ["demo-chat"], - cliBackends: ["demo-cli", "setup-cli"], - commandAliases: ["demo-command"], - contractKeys: ["tools"], - modelCatalogProviders: ["demo"], - providers: ["demo"], - setupProviders: ["demo"], origin: "global", rootDir: fixture.rootDir, source: path.join(fixture.rootDir, "index.ts"), diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index c7d2f9f6840..ca2e63af6f1 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -23,7 +23,6 @@ import { hasKind } from "./slots.js"; export const INSTALLED_PLUGIN_INDEX_VERSION = 1; export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 1; -export const INSTALLED_PLUGIN_CONTRIBUTION_METADATA_VERSION = 1; export const INSTALLED_PLUGIN_INDEX_WARNING = "DO NOT EDIT. This file is generated by OpenClaw from plugin manifests, install records, and config policy. Use `openclaw plugins registry --refresh`, `openclaw plugins install/update/uninstall`, or `openclaw plugins enable/disable` instead."; @@ -75,11 +74,6 @@ export type InstalledPluginPackageChannelInfo = Pick< export type InstalledPluginIndexRecord = { pluginId: string; - contributionMetadataVersion?: typeof INSTALLED_PLUGIN_CONTRIBUTION_METADATA_VERSION; - name?: string; - description?: string; - manifestVersion?: string; - legacyPluginIds?: readonly string[]; packageName?: string; packageVersion?: string; /** @@ -99,16 +93,6 @@ export type InstalledPluginIndexRecord = { manifestHash: string; format?: PluginManifestRecord["format"]; bundleFormat?: PluginManifestRecord["bundleFormat"]; - bundleCapabilities?: readonly string[]; - kind?: PluginManifestRecord["kind"]; - channels?: readonly string[]; - providers?: readonly string[]; - cliBackends?: readonly string[]; - setupProviders?: readonly string[]; - channelConfigs?: readonly string[]; - modelCatalogProviders?: readonly string[]; - commandAliases?: readonly string[]; - contractKeys?: readonly string[]; source?: string; setupSource?: string; packageJson?: { @@ -225,18 +209,6 @@ function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupI }; } -function collectContractKeys(record: PluginManifestRecord): readonly string[] | undefined { - const contracts = record.contracts; - if (!contracts) { - return undefined; - } - return normalizeStringList( - Object.entries(contracts).flatMap(([key, value]) => - Array.isArray(value) && value.length > 0 ? [key] : [], - ), - ); -} - function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompatCode[] { const codes: PluginCompatCode[] = []; if (record.providerAuthEnvVars && Object.keys(record.providerAuthEnvVars).length > 0) { @@ -335,10 +307,6 @@ function normalizeStringListField(value: unknown): readonly string[] | undefined return normalized.length > 0 ? normalized : undefined; } -function normalizeStringList(values: readonly string[] | undefined): readonly string[] | undefined { - return normalizeStringListField(values); -} - function normalizePackageChannel( channel: PluginPackageChannel | undefined, ): InstalledPluginPackageChannelInfo | undefined { @@ -608,7 +576,6 @@ function buildInstalledPluginIndex( }).enabled; const indexRecord: InstalledPluginIndexRecord = { pluginId: record.id, - contributionMetadataVersion: INSTALLED_PLUGIN_CONTRIBUTION_METADATA_VERSION, manifestPath: record.manifestPath, manifestHash, source: record.source, @@ -618,70 +585,12 @@ function buildInstalledPluginIndex( startup: buildStartupInfo(record), compat: collectCompatCodes(record), }; - if (record.name) { - indexRecord.name = record.name; - } - if (record.description) { - indexRecord.description = record.description; - } - if (record.version) { - indexRecord.manifestVersion = record.version; - } - const legacyPluginIds = normalizeStringList(record.legacyPluginIds); - if (legacyPluginIds) { - indexRecord.legacyPluginIds = legacyPluginIds; - } if (record.format && record.format !== "openclaw") { indexRecord.format = record.format; } if (record.bundleFormat) { indexRecord.bundleFormat = record.bundleFormat; } - if (record.bundleCapabilities?.length) { - indexRecord.bundleCapabilities = normalizeStringList(record.bundleCapabilities); - } - if (record.kind) { - indexRecord.kind = record.kind; - } - const channels = normalizeStringList(record.channels); - if (channels) { - indexRecord.channels = channels; - } - const providers = normalizeStringList(record.providers); - if (providers) { - indexRecord.providers = providers; - } - const cliBackends = normalizeStringList([ - ...record.cliBackends, - ...(record.setup?.cliBackends ?? []), - ]); - if (cliBackends) { - indexRecord.cliBackends = cliBackends; - } - const setupProviders = normalizeStringList( - record.setup?.providers?.map((provider) => provider.id), - ); - if (setupProviders) { - indexRecord.setupProviders = setupProviders; - } - const channelConfigs = normalizeStringList(Object.keys(record.channelConfigs ?? {})); - if (channelConfigs) { - indexRecord.channelConfigs = channelConfigs; - } - const modelCatalogProviders = normalizeStringList( - Object.keys(record.modelCatalog?.providers ?? {}), - ); - if (modelCatalogProviders) { - indexRecord.modelCatalogProviders = modelCatalogProviders; - } - const commandAliases = normalizeStringList(record.commandAliases?.map((alias) => alias.name)); - if (commandAliases) { - indexRecord.commandAliases = commandAliases; - } - const contractKeys = collectContractKeys(record); - if (contractKeys) { - indexRecord.contractKeys = contractKeys; - } if (record.enabledByDefault === true) { indexRecord.enabledByDefault = true; } diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index 9e3e0efb6a0..3020411627d 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -185,24 +185,6 @@ describe("plugin registry facade", () => { ).toEqual(["demo"]); }); - it("resolves indexed contribution owners without reopening manifest roots", () => { - const rootDir = makeTempDir(); - const candidate = createCandidate(rootDir); - const index = loadPluginRegistrySnapshot({ - candidates: [candidate], - env: hermeticEnv(), - preferPersisted: false, - }); - fs.rmSync(rootDir, { recursive: true, force: true }); - - expect(listPluginContributionIds({ index, contribution: "providers" })).toEqual(["demo"]); - expect(resolveProviderOwners({ index, providerId: "demo" })).toEqual(["demo"]); - expect(resolveChannelOwners({ index, channelId: "demo-chat" })).toEqual(["demo"]); - expect(resolveCliBackendOwners({ index, cliBackendId: "demo-setup-cli" })).toEqual(["demo"]); - expect(resolveSetupProviderOwners({ index, setupProviderId: "demo-setup" })).toEqual(["demo"]); - expect(createPluginRegistryIdNormalizer(index)("demo-chat")).toBe("demo"); - }); - it("keeps disabled records inspectable while excluding owners by default", () => { const rootDir = makeTempDir(); const candidate = createCandidate(rootDir); diff --git a/src/plugins/plugin-registry.ts b/src/plugins/plugin-registry.ts index 81998747f4a..207a3e35d96 100644 --- a/src/plugins/plugin-registry.ts +++ b/src/plugins/plugin-registry.ts @@ -12,7 +12,6 @@ import { type InstalledPluginIndexStoreOptions, } from "./installed-plugin-index-store.js"; import { - INSTALLED_PLUGIN_CONTRIBUTION_METADATA_VERSION, getInstalledPluginRecord, extractPluginInstallRecordsFromInstalledPluginIndex, isInstalledPluginEnabled, @@ -208,48 +207,6 @@ function listManifestContributionIds( return []; } -function hasIndexedContributionMetadata(plugin: InstalledPluginIndexRecord): boolean { - return plugin.contributionMetadataVersion === INSTALLED_PLUGIN_CONTRIBUTION_METADATA_VERSION; -} - -function listIndexedContributionIds( - plugin: InstalledPluginIndexRecord, - contribution: PluginRegistryContributionKey, -): readonly string[] { - switch (contribution) { - case "providers": - return plugin.providers ?? []; - case "channels": - return plugin.channels ?? []; - case "channelConfigs": - return plugin.channelConfigs ?? []; - case "setupProviders": - return plugin.setupProviders ?? []; - case "cliBackends": - return plugin.cliBackends ?? []; - case "modelCatalogProviders": - return plugin.modelCatalogProviders ?? []; - case "commandAliases": - return plugin.commandAliases ?? []; - case "contracts": - return plugin.contractKeys ?? []; - } - return []; -} - -function listContributionIndexRecords(params: { - index: PluginRegistrySnapshot; - includeDisabled?: boolean; - config?: OpenClawConfig; -}): readonly InstalledPluginIndexRecord[] { - if (params.includeDisabled) { - return params.index.plugins; - } - return params.index.plugins.filter((plugin) => - isInstalledPluginEnabled(params.index, plugin.pluginId, params.config), - ); -} - function resolveContributionPluginIds(params: { index: PluginRegistrySnapshot; includeDisabled?: boolean; @@ -307,35 +264,6 @@ export function createPluginRegistryIdNormalizer( aliases.set(normalizePluginRegistryAliasKey(pluginId), plugin.pluginId); } } - if (index.plugins.every(hasIndexedContributionMetadata)) { - for (const plugin of [...index.plugins].toSorted((left, right) => - left.pluginId.localeCompare(right.pluginId), - )) { - const pluginId = normalizePluginRegistryAlias(plugin.pluginId); - if (!pluginId) { - continue; - } - for (const alias of [ - plugin.pluginId, - ...(plugin.providers ?? []), - ...(plugin.channels ?? []), - ...(plugin.setupProviders ?? []), - ...(plugin.cliBackends ?? []), - ...(plugin.modelCatalogProviders ?? []), - ...(plugin.legacyPluginIds ?? []), - ]) { - const normalizedAlias = normalizePluginRegistryAlias(alias); - const normalizedAliasKey = normalizePluginRegistryAliasKey(alias); - if (normalizedAlias && !aliases.has(normalizedAliasKey)) { - aliases.set(normalizedAliasKey, pluginId); - } - } - } - return (pluginId: string) => { - const trimmed = normalizePluginRegistryAlias(pluginId); - return aliases.get(normalizePluginRegistryAliasKey(trimmed)) ?? trimmed; - }; - } const registry = loadPluginManifestRegistryForInstalledIndex({ index, includeDisabled: true, @@ -477,16 +405,6 @@ export function listPluginContributionIds( params: ListPluginContributionIdsParams, ): readonly string[] { const index = resolveSnapshot(params); - const records = listContributionIndexRecords({ - index, - includeDisabled: params.includeDisabled, - config: params.config, - }); - if (records.every(hasIndexedContributionMetadata)) { - return sortUnique( - records.flatMap((plugin) => listIndexedContributionIds(plugin, params.contribution)), - ); - } const registry = loadContributionManifestRegistry({ ...params, index, @@ -504,20 +422,6 @@ export function resolvePluginContributionOwners( ? (contributionId: string) => contributionId === params.matches : params.matches; const index = resolveSnapshot(params); - const records = listContributionIndexRecords({ - index, - includeDisabled: params.includeDisabled, - config: params.config, - }); - if (records.every(hasIndexedContributionMetadata)) { - return sortUnique( - records.flatMap((plugin) => - listIndexedContributionIds(plugin, params.contribution).some(matcher) - ? [plugin.pluginId] - : [], - ), - ); - } const registry = loadContributionManifestRegistry({ ...params, index, diff --git a/src/plugins/status.registry-snapshot.test.ts b/src/plugins/status.registry-snapshot.test.ts index ff6f927379f..245e0fd9c8f 100644 --- a/src/plugins/status.registry-snapshot.test.ts +++ b/src/plugins/status.registry-snapshot.test.ts @@ -3,8 +3,6 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { clearPluginDiscoveryCache } from "./discovery.js"; -import { writePersistedInstalledPluginIndex } from "./installed-plugin-index-store.js"; -import { loadInstalledPluginIndex } from "./installed-plugin-index.js"; import { clearPluginManifestRegistryCache } from "./manifest-registry.js"; import { buildPluginRegistrySnapshotReport } from "./status.js"; @@ -25,7 +23,7 @@ afterEach(() => { }); describe("buildPluginRegistrySnapshotReport", () => { - it("reports list metadata from the installed index without importing plugin runtime", () => { + it("reconstructs list metadata from indexed manifests without importing plugin runtime", () => { const pluginDir = makeTempDir(); const runtimeMarker = path.join(pluginDir, "runtime-loaded.txt"); fs.writeFileSync( @@ -82,71 +80,4 @@ describe("buildPluginRegistrySnapshotReport", () => { }); expect(fs.existsSync(runtimeMarker)).toBe(false); }); - - it("reports persisted indexed metadata without reopening stale manifest roots", async () => { - const pluginDir = makeTempDir(); - const stateDir = makeTempDir(); - const env = { - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_VERSION: "2026.4.25", - VITEST: "true", - }; - fs.writeFileSync( - path.join(pluginDir, "package.json"), - JSON.stringify({ - name: "@example/openclaw-stale-indexed-demo", - version: "4.5.6", - openclaw: { extensions: ["./index.cjs"] }, - }), - "utf-8", - ); - fs.writeFileSync( - path.join(pluginDir, "openclaw.plugin.json"), - JSON.stringify({ - id: "stale-indexed-demo", - name: "Stale Indexed Demo", - description: "Persisted list metadata", - version: "1.0.0", - providers: ["stale-provider"], - commandAliases: [{ name: "stale-command" }], - }), - "utf-8", - ); - fs.writeFileSync(path.join(pluginDir, "index.cjs"), "module.exports = {};\n", "utf-8"); - - const index = loadInstalledPluginIndex({ - config: {}, - env, - candidates: [ - { - idHint: "stale-indexed-demo", - source: path.join(pluginDir, "index.cjs"), - rootDir: pluginDir, - origin: "global", - packageName: "@example/openclaw-stale-indexed-demo", - packageVersion: "4.5.6", - packageDir: pluginDir, - }, - ], - }); - await writePersistedInstalledPluginIndex(index, { stateDir }); - fs.rmSync(pluginDir, { recursive: true, force: true }); - - const report = buildPluginRegistrySnapshotReport({ - config: {}, - env, - }); - - expect(report.registrySource).toBe("persisted"); - expect(report.plugins).toEqual([ - expect.objectContaining({ - id: "stale-indexed-demo", - name: "Stale Indexed Demo", - description: "Persisted list metadata", - version: "4.5.6", - providerIds: ["stale-provider"], - commands: ["stale-command"], - }), - ]); - }); }); diff --git a/src/plugins/status.ts b/src/plugins/status.ts index d4475fca0b9..423ef68c1f7 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -18,6 +18,8 @@ import { type PluginInspectShape, } from "./inspect-shape.js"; import { loadOpenClawPlugins } from "./loader.js"; +import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; +import type { PluginManifestRecord } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; import { loadPluginRegistrySnapshotWithMetadata, @@ -155,22 +157,20 @@ type PluginReportParams = { function buildPluginRecordFromInstalledIndex( plugin: import("./installed-plugin-index.js").InstalledPluginIndexRecord, + manifest?: PluginManifestRecord, ): PluginRecord { - const format = plugin.format ?? "openclaw"; - const bundleFormat = plugin.bundleFormat; + const format = plugin.format ?? manifest?.format ?? "openclaw"; + const bundleFormat = plugin.bundleFormat ?? manifest?.bundleFormat; return { id: plugin.pluginId, - name: plugin.name ?? plugin.packageName ?? plugin.pluginId, - ...(plugin.packageVersion || plugin.manifestVersion - ? { version: plugin.packageVersion ?? plugin.manifestVersion } + name: manifest?.name ?? plugin.packageName ?? plugin.pluginId, + ...(plugin.packageVersion || manifest?.version + ? { version: plugin.packageVersion ?? manifest?.version } : {}), - ...(plugin.description ? { description: plugin.description } : {}), + ...(manifest?.description ? { description: manifest.description } : {}), format, ...(bundleFormat ? { bundleFormat } : {}), - ...(plugin.bundleCapabilities?.length - ? { bundleCapabilities: [...plugin.bundleCapabilities] } - : {}), - ...(plugin.kind ? { kind: plugin.kind } : {}), + ...(manifest?.kind ? { kind: manifest.kind } : {}), source: plugin.source ?? plugin.manifestPath, rootDir: plugin.rootDir, origin: plugin.origin, @@ -178,9 +178,9 @@ function buildPluginRecordFromInstalledIndex( status: plugin.enabled ? "loaded" : "disabled", toolNames: [], hookNames: [], - channelIds: [...(plugin.channels ?? [])], - cliBackendIds: [...(plugin.cliBackends ?? [])], - providerIds: [...(plugin.providers ?? [])], + channelIds: [...(manifest?.channels ?? [])], + cliBackendIds: [...(manifest?.cliBackends ?? []), ...(manifest?.setup?.cliBackends ?? [])], + providerIds: [...(manifest?.providers ?? [])], speechProviderIds: [], realtimeTranscriptionProviderIds: [], realtimeVoiceProviderIds: [], @@ -196,7 +196,7 @@ function buildPluginRecordFromInstalledIndex( cliCommands: [], services: [], gatewayDiscoveryServiceIds: [], - commands: [...(plugin.commandAliases ?? [])], + commands: [...(manifest?.commandAliases?.map((alias) => alias.name) ?? [])], httpRoutes: 0, hookCount: 0, configSchema: false, @@ -213,10 +213,20 @@ export function buildPluginRegistrySnapshotReport( env: params?.env, workspaceDir: params?.workspaceDir, }); + const manifestRegistry = loadPluginManifestRegistryForInstalledIndex({ + index: result.snapshot, + config, + env: params?.env, + workspaceDir: params?.workspaceDir, + includeDisabled: true, + }); + const manifestByPluginId = new Map(manifestRegistry.plugins.map((plugin) => [plugin.id, plugin])); return { workspaceDir: params?.workspaceDir, ...createEmptyPluginRegistry(), - plugins: result.snapshot.plugins.map((plugin) => buildPluginRecordFromInstalledIndex(plugin)), + plugins: result.snapshot.plugins.map((plugin) => + buildPluginRecordFromInstalledIndex(plugin, manifestByPluginId.get(plugin.pluginId)), + ), diagnostics: [...result.snapshot.diagnostics], registrySource: result.source, registryDiagnostics: result.diagnostics,