Files
openclaw/src/plugins/config-contracts.ts
Dallin Romney 88d8d6af93 perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts (#84283)
* perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts

Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult
through the remaining helpers that still call discoverOpenClawPlugins
internally during startup:

- loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts):
  add discovery? to PluginLoadOptions and consult it before falling back to
  an internal scan at both call sites.

- loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept
  discovery? as a more ergonomic alternative to the existing candidates? /
  diagnostics? pair; candidates? still wins when both are supplied.

- resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts):
  add discovery? to LoadInstalledPluginIndexParams and use it when
  candidates aren't supplied.

- resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add
  discovery? and thread it into the bundled-fallback discovery call.

Add discovery-threading.test.ts asserting each entry point skips its
internal discoverOpenClawPlugins call when discovery is supplied, calls it
when nothing is supplied, and prefers explicit candidates over discovery
when both are present (6 tests, all pass).

discoverOpenClawPlugins remains stateless; sharing is function-scoped per
src/plugins/CLAUDE.md guidance. Backward compatible: every change is
additive (new optional param).

* perf(plugins): drop verbose JSDoc from discovery? params
2026-05-19 16:22:30 -07:00

226 lines
7.0 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isRecord } from "../utils.js";
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import type { PluginManifestConfigContracts } from "./manifest.js";
import type { PluginOrigin } from "./plugin-origin.types.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
export type PluginConfigContractMatch = {
path: string;
value: unknown;
};
export type PluginConfigContractMetadata = {
origin: PluginOrigin;
configContracts: PluginManifestConfigContracts;
};
type TraversalState = {
segments: string[];
value: unknown;
};
function normalizePathPattern(pathPattern: string): string[] {
return pathPattern
.split(".")
.map((segment) => segment.trim())
.filter(Boolean);
}
function appendPathSegment(path: string, segment: string): string {
if (!path) {
return segment;
}
return /^\d+$/.test(segment) ? `${path}[${segment}]` : `${path}.${segment}`;
}
function parseCanonicalArrayIndex(segment: string, length: number): number | null {
if (!/^(0|[1-9]\d*)$/.test(segment)) {
return null;
}
const index = Number(segment);
return Number.isSafeInteger(index) && index >= 0 && index < length ? index : null;
}
export function collectPluginConfigContractMatches(params: {
root: unknown;
pathPattern: string;
}): PluginConfigContractMatch[] {
const pattern = normalizePathPattern(params.pathPattern);
if (pattern.length === 0) {
return [];
}
let states: TraversalState[] = [{ segments: [], value: params.root }];
for (const segment of pattern) {
const nextStates: TraversalState[] = [];
for (const state of states) {
if (segment === "*") {
if (Array.isArray(state.value)) {
for (const [index, value] of state.value.entries()) {
nextStates.push({
segments: [...state.segments, String(index)],
value,
});
}
continue;
}
if (isRecord(state.value)) {
for (const [key, value] of Object.entries(state.value)) {
nextStates.push({
segments: [...state.segments, key],
value,
});
}
}
continue;
}
if (Array.isArray(state.value)) {
const index = parseCanonicalArrayIndex(segment, state.value.length);
if (index !== null) {
nextStates.push({
segments: [...state.segments, segment],
value: state.value[index],
});
}
continue;
}
if (!isRecord(state.value) || !Object.prototype.hasOwnProperty.call(state.value, segment)) {
continue;
}
nextStates.push({
segments: [...state.segments, segment],
value: state.value[segment],
});
}
states = nextStates;
if (states.length === 0) {
break;
}
}
return states.map((state) => ({
path: state.segments.reduce(appendPathSegment, ""),
value: state.value,
}));
}
export function resolvePluginConfigContractsById(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
fallbackToBundledMetadata?: boolean;
fallbackToBundledMetadataForResolvedBundled?: boolean;
fallbackBundledPluginIds?: readonly string[];
pluginIds: readonly string[];
discovery?: PluginDiscoveryResult;
}): ReadonlyMap<string, PluginConfigContractMetadata> {
const matches = new Map<string, PluginConfigContractMetadata>();
const pluginIds = [
...new Set(params.pluginIds.map((pluginId) => pluginId.trim()).filter(Boolean)),
];
if (pluginIds.length === 0) {
return matches;
}
const fallbackBundledPluginIds = new Set(
(params.fallbackBundledPluginIds ?? []).map((pluginId) => pluginId.trim()).filter(Boolean),
);
const bundledContractFallbacks = new Map<string, PluginManifestConfigContracts | undefined>();
const findBundledConfigContracts = (
pluginId: string,
): PluginManifestConfigContracts | undefined => {
if (bundledContractFallbacks.has(pluginId)) {
return bundledContractFallbacks.get(pluginId);
}
const discovery =
params.discovery ??
discoverOpenClawPlugins({
workspaceDir: params.workspaceDir,
env: params.env,
});
const registry = loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
candidates: discovery.candidates.filter((candidate) => candidate.origin === "bundled"),
diagnostics: discovery.diagnostics,
});
for (const plugin of registry.plugins) {
bundledContractFallbacks.set(plugin.id, plugin.configContracts);
}
if (!bundledContractFallbacks.has(pluginId)) {
bundledContractFallbacks.set(pluginId, undefined);
}
return bundledContractFallbacks.get(pluginId);
};
const resolvedPluginOrigins = new Map<string, PluginOrigin>();
const registry = loadPluginManifestRegistryForPluginRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
includeDisabled: true,
});
for (const plugin of registry.plugins) {
if (!pluginIds.includes(plugin.id)) {
continue;
}
resolvedPluginOrigins.set(plugin.id, plugin.origin);
if (!plugin.configContracts) {
continue;
}
matches.set(plugin.id, {
origin: plugin.origin,
configContracts: plugin.configContracts,
});
}
if (params.fallbackToBundledMetadata ?? true) {
for (const pluginId of pluginIds) {
const existing = matches.get(pluginId);
const shouldHydrateBundledMatch =
existing &&
((params.fallbackToBundledMetadataForResolvedBundled && existing.origin === "bundled") ||
fallbackBundledPluginIds.has(pluginId));
if (shouldHydrateBundledMatch) {
const bundledConfigContracts = findBundledConfigContracts(pluginId);
if (bundledConfigContracts) {
matches.set(pluginId, {
origin: fallbackBundledPluginIds.has(pluginId) ? "bundled" : existing.origin,
configContracts: {
...bundledConfigContracts,
...existing.configContracts,
...(bundledConfigContracts.secretInputs
? { secretInputs: bundledConfigContracts.secretInputs }
: {}),
},
});
}
continue;
}
if (matches.has(pluginId)) {
continue;
}
const resolvedOrigin = resolvedPluginOrigins.get(pluginId);
if (
resolvedOrigin &&
!(params.fallbackToBundledMetadataForResolvedBundled && resolvedOrigin === "bundled") &&
!fallbackBundledPluginIds.has(pluginId)
) {
continue;
}
const bundledConfigContracts = findBundledConfigContracts(pluginId);
if (!bundledConfigContracts) {
continue;
}
matches.set(pluginId, {
origin: "bundled",
configContracts: bundledConfigContracts,
});
}
}
return matches;
}