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.
This commit is contained in:
Vincent Koc
2026-04-26 18:58:04 -07:00
committed by GitHub
parent c45a7d7a7a
commit 8c2bc951a9
7 changed files with 142 additions and 2 deletions

View File

@@ -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;

View File

@@ -368,6 +368,7 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
cache: false,
env,
config: {},
bundledChannelConfigCollector: runtime.collectBundledChannelConfigs,
});
logConfigDocBaselineDebug(`loaded ${manifestRegistry.plugins.length} bundled plugin manifests`);
const bundledRegistry = {

View File

@@ -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,
});
}

View File

@@ -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 }
: {}),
});
}

View File

@@ -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, {

View File

@@ -158,6 +158,12 @@ export type PluginManifestRegistry = {
diagnostics: PluginDiagnostic[];
};
export type BundledChannelConfigCollector = (params: {
pluginDir: string;
manifest: PluginManifest;
packageManifest?: OpenClawPackageManifest;
}) => Record<string, PluginManifestChannelConfig> | undefined;
const registryCache = pluginManifestRegistryCache as Map<
string,
{ expiresAt: number; registry: PluginManifestRegistry }
@@ -293,9 +299,18 @@ function buildRecord(params: {
manifestPath: string;
schemaCacheKey?: string;
configSchema?: Record<string, unknown>;
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<string, PluginInstallRecord>;
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);

View File

@@ -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 }
: {}),
});
}