From 8c2bc951a9438b48e82bd270a61819139197e68d Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 18:58:04 -0700 Subject: [PATCH] fix(plugins): hydrate bundled channel config metadata Hydrate bundled channel schema metadata through opt-in registry schema paths while keeping ordinary manifest registry loads lightweight. --- src/config/doc-baseline.runtime.ts | 2 + src/config/doc-baseline.ts | 1 + src/config/runtime-schema.ts | 2 + src/plugins/manifest-registry-installed.ts | 5 + src/plugins/manifest-registry.test.ts | 103 +++++++++++++++++++ src/plugins/manifest-registry.ts | 26 ++++- src/plugins/plugin-registry-contributions.ts | 5 + 7 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/config/doc-baseline.runtime.ts b/src/config/doc-baseline.runtime.ts index 62474bef75e..6180b6a1b9b 100644 --- a/src/config/doc-baseline.runtime.ts +++ b/src/config/doc-baseline.runtime.ts @@ -1,3 +1,4 @@ +import { collectBundledChannelConfigs as collectBundledChannelConfigsImpl } from "../plugins/bundled-channel-config-metadata.js"; import { loadPluginManifestRegistry as loadPluginManifestRegistryImpl } from "../plugins/manifest-registry.js"; import { collectChannelSchemaMetadata as collectChannelSchemaMetadataImpl, @@ -6,6 +7,7 @@ import { import { buildConfigSchema as buildConfigSchemaImpl } from "./schema.js"; export const loadPluginManifestRegistry = loadPluginManifestRegistryImpl; +export const collectBundledChannelConfigs = collectBundledChannelConfigsImpl; export const collectChannelSchemaMetadata = collectChannelSchemaMetadataImpl; export const collectPluginSchemaMetadata = collectPluginSchemaMetadataImpl; export const buildConfigSchema = buildConfigSchemaImpl; diff --git a/src/config/doc-baseline.ts b/src/config/doc-baseline.ts index 06d30e7e21c..1c18edc4d0b 100644 --- a/src/config/doc-baseline.ts +++ b/src/config/doc-baseline.ts @@ -368,6 +368,7 @@ async function loadBundledConfigSchemaResponse(): Promise cache: false, env, config: {}, + bundledChannelConfigCollector: runtime.collectBundledChannelConfigs, }); logConfigDocBaselineDebug(`loaded ${manifestRegistry.plugins.length} bundled plugin manifests`); const bundledRegistry = { diff --git a/src/config/runtime-schema.ts b/src/config/runtime-schema.ts index b32313fe2b7..ccf15809c15 100644 --- a/src/config/runtime-schema.ts +++ b/src/config/runtime-schema.ts @@ -1,4 +1,5 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { collectBundledChannelConfigs } from "../plugins/bundled-channel-config-metadata.js"; import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; import { collectChannelSchemaMetadata, @@ -16,6 +17,7 @@ function loadManifestRegistry(config: OpenClawConfig, env?: NodeJS.ProcessEnv) { env, workspaceDir, includeDisabled: true, + bundledChannelConfigCollector: collectBundledChannelConfigs, }); } diff --git a/src/plugins/manifest-registry-installed.ts b/src/plugins/manifest-registry-installed.ts index 747cf048727..976f91768fd 100644 --- a/src/plugins/manifest-registry-installed.ts +++ b/src/plugins/manifest-registry-installed.ts @@ -5,6 +5,7 @@ import type { PluginCandidate } from "./discovery.js"; import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js"; import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js"; import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js"; +import type { BundledChannelConfigCollector } from "./manifest-registry.js"; import { DEFAULT_PLUGIN_ENTRY_CANDIDATES, getPackageManifestMetadata, @@ -88,6 +89,7 @@ export function loadPluginManifestRegistryForInstalledIndex(params: { env?: NodeJS.ProcessEnv; pluginIds?: readonly string[]; includeDisabled?: boolean; + bundledChannelConfigCollector?: BundledChannelConfigCollector; }): PluginManifestRegistry { if (params.pluginIds && params.pluginIds.length === 0) { return { plugins: [], diagnostics: [] }; @@ -111,5 +113,8 @@ export function loadPluginManifestRegistryForInstalledIndex(params: { candidates, diagnostics: [...diagnostics], installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(params.index), + ...(params.bundledChannelConfigCollector + ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } + : {}), }); } diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index 058f44cc647..a2279c8dca3 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -1,6 +1,8 @@ import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { collectChannelSchemaMetadata } from "../config/channel-config-metadata.js"; +import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js"; import type { PluginCandidate } from "./discovery.js"; import { clearPluginManifestRegistryCache, @@ -630,6 +632,107 @@ describe("loadPluginManifestRegistry", () => { }); }); + it("hydrates bundled channel config metadata from plugin-local config surfaces", () => { + const dir = makeTempDir(); + writeManifest(dir, { + id: "alpha", + channels: ["alpha"], + configSchema: { type: "object" }, + channelConfigs: { + alpha: { + schema: { + type: "object", + properties: { + manifestOnly: { type: "boolean" }, + }, + }, + uiHints: { + manifestOnly: { help: "manifest hint" }, + }, + }, + }, + }); + writeTextFile(dir, "index.ts", "export {};\n"); + writeTextFile( + dir, + "src/config-schema.js", + [ + "export const AlphaChannelConfigSchema = {", + " schema: {", + " type: 'object',", + " properties: {", + " generatedOnly: { type: 'string' },", + " },", + " additionalProperties: false,", + " },", + " uiHints: {", + " generatedOnly: { label: 'Generated only' },", + " },", + "};", + ].join("\n"), + ); + + const candidate = createPluginCandidate({ + idHint: "alpha", + rootDir: dir, + origin: "bundled", + packageDir: dir, + packageManifest: { + channel: { + id: "alpha", + label: "Alpha", + blurb: "Alpha channel", + }, + }, + }); + expect(loadRegistry([candidate]).plugins[0]?.channelConfigs?.alpha?.schema).toEqual({ + type: "object", + properties: { + manifestOnly: { type: "boolean" }, + }, + }); + + const registry = loadPluginManifestRegistry({ + cache: false, + bundledChannelConfigCollector: collectBundledChannelConfigs, + candidates: [candidate], + }); + + expect(registry.plugins[0]?.channelConfigs?.alpha).toEqual({ + schema: { + type: "object", + properties: { + generatedOnly: { type: "string" }, + }, + additionalProperties: false, + }, + label: "Alpha", + description: "Alpha channel", + uiHints: { + generatedOnly: { label: "Generated only" }, + manifestOnly: { help: "manifest hint" }, + }, + }); + expect(collectChannelSchemaMetadata(registry)).toEqual([ + { + id: "alpha", + label: "Alpha", + description: "Alpha channel", + configSchema: { + type: "object", + properties: { + generatedOnly: { type: "string" }, + }, + additionalProperties: false, + }, + configUiHints: { + generatedOnly: { label: "Generated only" }, + manifestOnly: { help: "manifest hint" }, + }, + }, + ]); + }); + it("reports non-bundled providerAuthEnvVars as deprecated compat metadata", () => { const dir = makeTempDir(); writeManifest(dir, { diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 5e20d3039f1..51ecf2d88d7 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -158,6 +158,12 @@ export type PluginManifestRegistry = { diagnostics: PluginDiagnostic[]; }; +export type BundledChannelConfigCollector = (params: { + pluginDir: string; + manifest: PluginManifest; + packageManifest?: OpenClawPackageManifest; +}) => Record | undefined; + const registryCache = pluginManifestRegistryCache as Map< string, { expiresAt: number; registry: PluginManifestRegistry } @@ -293,9 +299,18 @@ function buildRecord(params: { manifestPath: string; schemaCacheKey?: string; configSchema?: Record; + bundledChannelConfigCollector?: BundledChannelConfigCollector; }): PluginManifestRecord { + const manifestChannelConfigs = + params.candidate.origin === "bundled" && params.bundledChannelConfigCollector + ? params.bundledChannelConfigCollector({ + pluginDir: params.candidate.packageDir ?? params.candidate.rootDir, + manifest: params.manifest, + packageManifest: params.candidate.packageManifest, + }) + : params.manifest.channelConfigs; const channelConfigs = mergePackageChannelMetaIntoChannelConfigs({ - channelConfigs: params.manifest.channelConfigs, + channelConfigs: manifestChannelConfigs, packageChannel: params.candidate.packageManifest?.channel, }); const packageChannelCommands = normalizePackageChannelCommands( @@ -542,6 +557,7 @@ export function loadPluginManifestRegistry( candidates?: PluginCandidate[]; diagnostics?: PluginDiagnostic[]; installRecords?: Record; + bundledChannelConfigCollector?: BundledChannelConfigCollector; } = {}, ): PluginManifestRegistry { const config = params.config ?? {}; @@ -549,7 +565,10 @@ export function loadPluginManifestRegistry( const env = params.env ?? process.env; const cacheKey = buildCacheKey({ workspaceDir: params.workspaceDir, plugins: normalized, env }); const cacheEnabled = - params.cache !== false && !params.installRecords && shouldUseManifestCache(env); + params.cache !== false && + !params.installRecords && + !params.bundledChannelConfigCollector && + shouldUseManifestCache(env); if (cacheEnabled) { const cached = registryCache.get(cacheKey); if (cached && cached.expiresAt > Date.now()) { @@ -659,6 +678,9 @@ export function loadPluginManifestRegistry( manifestPath: manifestRes.manifestPath, schemaCacheKey, configSchema, + ...(params.bundledChannelConfigCollector + ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } + : {}), }); const existing = seenIds.get(manifest.id); diff --git a/src/plugins/plugin-registry-contributions.ts b/src/plugins/plugin-registry-contributions.ts index af2938f168b..0460d505944 100644 --- a/src/plugins/plugin-registry-contributions.ts +++ b/src/plugins/plugin-registry-contributions.ts @@ -7,6 +7,7 @@ import { import { isInstalledPluginEnabled } from "./installed-plugin-index.js"; import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js"; import type { + BundledChannelConfigCollector, PluginManifestContractListKey, PluginManifestRecord, PluginManifestRegistry, @@ -25,6 +26,7 @@ export type PluginRegistryContributionOptions = LoadPluginRegistryParams & { export type LoadPluginRegistryManifestParams = LoadPluginRegistryParams & { includeDisabled?: boolean; pluginIds?: readonly string[]; + bundledChannelConfigCollector?: BundledChannelConfigCollector; }; export type PluginRegistryContributionKey = @@ -201,6 +203,9 @@ export function loadPluginManifestRegistryForPluginRegistry( env: params.env, pluginIds: params.pluginIds, includeDisabled: params.includeDisabled, + ...(params.bundledChannelConfigCollector + ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } + : {}), }); }