feat(plugins): resolve provider owners from registry

This commit is contained in:
Vincent Koc
2026-04-25 05:45:37 -07:00
parent 674d188153
commit 29988335fc
3 changed files with 162 additions and 73 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc.
- Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc.
- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc.
- Providers/plugins: resolve provider ownership, provider discovery scopes, and catalog-hook provider ids from the cold plugin registry instead of rescanning manifests on those paths. Thanks @vincentkoc.
- Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna.
- Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna.

View File

@@ -311,6 +311,13 @@ describe("resolvePluginProviders", () => {
resolveManifestContractPluginIds: (...args: Parameters<ResolveManifestContractPluginIds>) =>
resolveManifestContractPluginIdsMock(...args),
}));
vi.doMock("./installed-plugin-index-store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./installed-plugin-index-store.js")>();
return {
...actual,
readPersistedInstalledPluginIndexSync: () => null,
};
});
({
resolveActivatableProviderOwnerPluginIds,
resolveOwningPluginIdsForProvider,

View File

@@ -8,10 +8,16 @@ import {
} from "./manifest-owner-policy.js";
import {
loadPluginManifestRegistry,
resolveManifestContractPluginIds,
type PluginManifestRecord,
type PluginManifestRegistry,
} from "./manifest-registry.js";
import {
loadPluginRegistrySnapshot,
resolvePluginContributionOwners,
resolveProviderOwners,
type PluginRegistryRecord,
type PluginRegistrySnapshot,
} from "./plugin-registry.js";
import { createPluginIdScopeSet } from "./plugin-scope.js";
type ProviderManifestLoadParams = {
@@ -20,6 +26,9 @@ type ProviderManifestLoadParams = {
env?: PluginLoadOptions["env"];
};
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfig>;
type ProviderRegistryLoadParams = ProviderManifestLoadParams & {
onlyPluginIds?: readonly string[];
};
function loadProviderManifestRegistry(params: ProviderManifestLoadParams): PluginManifestRegistry {
return loadPluginManifestRegistry({
@@ -29,33 +38,39 @@ function loadProviderManifestRegistry(params: ProviderManifestLoadParams): Plugi
});
}
function loadScopedProviderManifestRegistry(
params: ProviderManifestLoadParams & { onlyPluginIds?: readonly string[] },
): {
registry: PluginManifestRegistry;
function loadProviderRegistrySnapshot(params: ProviderManifestLoadParams): PluginRegistrySnapshot {
return loadPluginRegistrySnapshot({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
}
function loadScopedProviderRegistry(params: ProviderRegistryLoadParams): {
registry: PluginRegistrySnapshot;
onlyPluginIdSet: ReturnType<typeof createPluginIdScopeSet>;
} {
return {
registry: loadProviderManifestRegistry(params),
registry: loadProviderRegistrySnapshot(params),
onlyPluginIdSet: createPluginIdScopeSet(params.onlyPluginIds),
};
}
function listManifestPluginIds(
registry: PluginManifestRegistry,
predicate: (plugin: PluginManifestRecord) => boolean,
function listRegistryPluginIds(
registry: PluginRegistrySnapshot,
predicate: (plugin: PluginRegistryRecord) => boolean,
): string[] {
return registry.plugins
.filter(predicate)
.map((plugin) => plugin.id)
.map((plugin) => plugin.pluginId)
.toSorted((left, right) => left.localeCompare(right));
}
function resolveProviderOwnerPluginIds(
params: ProviderManifestLoadParams & {
params: ProviderRegistryLoadParams & {
pluginIds: readonly string[];
isEligible: (
plugin: PluginManifestRecord,
plugin: PluginRegistryRecord,
normalizedConfig: NormalizedPluginsConfig,
) => boolean;
},
@@ -64,14 +79,40 @@ function resolveProviderOwnerPluginIds(
return [];
}
const pluginIdSet = new Set(params.pluginIds);
const registry = loadProviderManifestRegistry(params);
const registry = loadProviderRegistrySnapshot(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(
return listRegistryPluginIds(
registry,
(plugin) => pluginIdSet.has(plugin.id) && params.isEligible(plugin, normalizedConfig),
(plugin) => pluginIdSet.has(plugin.pluginId) && params.isEligible(plugin, normalizedConfig),
);
}
function recordHasProviderSurface(plugin: PluginRegistryRecord): boolean {
return plugin.contributions.providers.length > 0;
}
function resolveEffectiveRegistryPluginActivation(params: {
plugin: PluginRegistryRecord;
normalizedConfig: NormalizedPluginsConfig;
rootConfig?: PluginLoadOptions["config"];
}) {
return resolveEffectivePluginActivationState({
id: params.plugin.pluginId,
origin: params.plugin.origin,
config: params.normalizedConfig,
rootConfig: params.rootConfig,
enabledByDefault: params.plugin.enabledByDefault,
});
}
function toManifestOwnerRecord(plugin: PluginRegistryRecord) {
return {
id: plugin.pluginId,
origin: plugin.origin,
enabledByDefault: plugin.enabledByDefault,
};
}
export function withBundledProviderVitestCompat(params: {
config: PluginLoadOptions["config"];
pluginIds: readonly string[];
@@ -86,13 +127,13 @@ export function resolveBundledProviderCompatPluginIds(params: {
env?: PluginLoadOptions["env"];
onlyPluginIds?: readonly string[];
}): string[] {
const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params);
return listManifestPluginIds(
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
return listRegistryPluginIds(
registry,
(plugin) =>
plugin.origin === "bundled" &&
plugin.providers.length > 0 &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
recordHasProviderSurface(plugin) &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)),
);
}
@@ -102,19 +143,17 @@ export function resolveEnabledProviderPluginIds(params: {
env?: PluginLoadOptions["env"];
onlyPluginIds?: readonly string[];
}): string[] {
const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params);
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(
return listRegistryPluginIds(
registry,
(plugin) =>
plugin.providers.length > 0 &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) &&
resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: normalizedConfig,
recordHasProviderSurface(plugin) &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId)) &&
resolveEffectiveRegistryPluginActivation({
plugin,
normalizedConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
}).activated,
);
}
@@ -124,11 +163,29 @@ export function resolveExternalAuthProfileProviderPluginIds(params: {
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
return resolveManifestContractPluginIds({
return resolveRegistryManifestContractPluginIds({
...params,
contract: "externalAuthProviders",
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
}
function resolveRegistryManifestContractPluginIds(params: {
contract: string;
config?: PluginLoadOptions["config"];
workspaceDir?: string;
env?: PluginLoadOptions["env"];
origin?: PluginRegistryRecord["origin"];
onlyPluginIds?: readonly string[];
}): string[] {
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
return listRegistryPluginIds(registry, (plugin) => {
if (params.origin && plugin.origin !== params.origin) {
return false;
}
if (onlyPluginIdSet && !onlyPluginIdSet.has(plugin.pluginId)) {
return false;
}
return plugin.contributions.contracts.includes(params.contract);
});
}
@@ -143,14 +200,14 @@ export function resolveExternalAuthProfileCompatFallbackPluginIds(params: {
// this with the warning path in provider-runtime after the migration window.
const declaredPluginIds =
params.declaredPluginIds ?? new Set(resolveExternalAuthProfileProviderPluginIds(params));
const registry = loadProviderManifestRegistry(params);
const registry = loadProviderRegistrySnapshot(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(
return listRegistryPluginIds(
registry,
(plugin) =>
plugin.origin !== "bundled" &&
plugin.providers.length > 0 &&
!declaredPluginIds.has(plugin.id) &&
recordHasProviderSurface(plugin) &&
!declaredPluginIds.has(plugin.pluginId) &&
isProviderPluginEligibleForRuntimeOwnerActivation({
plugin,
normalizedConfig,
@@ -166,11 +223,16 @@ export function resolveDiscoveredProviderPluginIds(params: {
onlyPluginIds?: readonly string[];
includeUntrustedWorkspacePlugins?: boolean;
}): string[] {
const { registry, onlyPluginIdSet } = loadScopedProviderManifestRegistry(params);
const { registry, onlyPluginIdSet } = loadScopedProviderRegistry(params);
const shouldFilterUntrustedWorkspacePlugins = params.includeUntrustedWorkspacePlugins === false;
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(registry, (plugin) => {
if (!(plugin.providers.length > 0 && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)))) {
return listRegistryPluginIds(registry, (plugin) => {
if (
!(
recordHasProviderSurface(plugin) &&
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.pluginId))
)
) {
return false;
}
return isProviderPluginEligibleForSetupDiscovery({
@@ -183,7 +245,7 @@ export function resolveDiscoveredProviderPluginIds(params: {
}
function isProviderPluginEligibleForSetupDiscovery(params: {
plugin: PluginManifestRecord;
plugin: PluginRegistryRecord;
shouldFilterUntrustedWorkspacePlugins: boolean;
normalizedConfig: NormalizedPluginsConfig;
rootConfig?: PluginLoadOptions["config"];
@@ -193,14 +255,14 @@ function isProviderPluginEligibleForSetupDiscovery(params: {
}
if (
!passesManifestOwnerBasePolicy({
plugin: params.plugin,
plugin: toManifestOwnerRecord(params.plugin),
normalizedConfig: params.normalizedConfig,
})
) {
return false;
}
return isActivatedManifestOwner({
plugin: params.plugin,
plugin: toManifestOwnerRecord(params.plugin),
normalizedConfig: params.normalizedConfig,
rootConfig: params.rootConfig,
});
@@ -227,13 +289,13 @@ export function resolveDiscoverableProviderOwnerPluginIds(params: {
}
function isProviderPluginEligibleForRuntimeOwnerActivation(params: {
plugin: PluginManifestRecord;
plugin: PluginRegistryRecord;
normalizedConfig: NormalizedPluginsConfig;
rootConfig?: PluginLoadOptions["config"];
}): boolean {
if (
!passesManifestOwnerBasePolicy({
plugin: params.plugin,
plugin: toManifestOwnerRecord(params.plugin),
normalizedConfig: params.normalizedConfig,
})
) {
@@ -243,7 +305,7 @@ function isProviderPluginEligibleForRuntimeOwnerActivation(params: {
return true;
}
return isActivatedManifestOwner({
plugin: params.plugin,
plugin: toManifestOwnerRecord(params.plugin),
normalizedConfig: params.normalizedConfig,
rootConfig: params.rootConfig,
});
@@ -376,20 +438,42 @@ export function resolveOwningPluginIdsForProvider(params: {
return undefined;
}
const registry = resolveManifestRegistry(params);
const pluginIds = registry.plugins
.filter(
(plugin) =>
plugin.providers.some(
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
) ||
plugin.cliBackends.some(
(backendId) => normalizeProviderId(backendId) === normalizedProvider,
),
)
.map((plugin) => plugin.id);
if (params.manifestRegistry) {
const pluginIds = params.manifestRegistry.plugins
.filter(
(plugin) =>
plugin.providers.some(
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
) ||
plugin.cliBackends.some(
(backendId) => normalizeProviderId(backendId) === normalizedProvider,
),
)
.map((plugin) => plugin.id);
return pluginIds.length > 0 ? pluginIds : undefined;
return pluginIds.length > 0 ? pluginIds : undefined;
}
const pluginIds = [
...resolveProviderOwners({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
providerId: normalizedProvider,
includeDisabled: true,
}),
...resolvePluginContributionOwners({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
contribution: "cliBackends",
matches: (backendId) => normalizeProviderId(backendId) === normalizedProvider,
includeDisabled: true,
}),
];
const deduped = dedupeSortedPluginIds(pluginIds);
return deduped.length > 0 ? deduped : undefined;
}
export function resolveOwningPluginIdsForModelRef(params: {
@@ -456,17 +540,16 @@ export function resolveNonBundledProviderPluginIds(params: {
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
const registry = loadProviderManifestRegistry(params);
const registry = loadProviderRegistrySnapshot(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
return listManifestPluginIds(
return listRegistryPluginIds(
registry,
(plugin) =>
plugin.origin !== "bundled" &&
plugin.providers.length > 0 &&
resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: normalizedConfig,
recordHasProviderSurface(plugin) &&
resolveEffectiveRegistryPluginActivation({
plugin,
normalizedConfig,
rootConfig: params.config,
}).activated,
);
@@ -477,18 +560,16 @@ export function resolveCatalogHookProviderPluginIds(params: {
workspaceDir?: string;
env?: PluginLoadOptions["env"];
}): string[] {
const registry = loadProviderManifestRegistry(params);
const registry = loadProviderRegistrySnapshot(params);
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
const enabledProviderPluginIds = listManifestPluginIds(
const enabledProviderPluginIds = listRegistryPluginIds(
registry,
(plugin) =>
plugin.providers.length > 0 &&
resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: normalizedConfig,
recordHasProviderSurface(plugin) &&
resolveEffectiveRegistryPluginActivation({
plugin,
normalizedConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
}).activated,
);
const bundledCompatPluginIds = resolveBundledProviderCompatPluginIds(params);