openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
826 lines (825 loc) • 37.3 kB
JavaScript
import { t as sanitizeForLog } from "./ansi-DrXAcdMD.js";
import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { s as normalizeOptionalTrimmedStringList, y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js";
import { n as isPathInside } from "./path-safety-Bi0ppMWC.js";
import { p as redactSensitiveText } from "./redact-BtvPPfTi.js";
import "./utils-P__uGsPB.js";
import { i as getGatewayPluginMetadataSnapshot } from "./current-plugin-metadata-state-B1UoAr4G.js";
import { a as pluginCacheRealpathSync, c as readPluginCacheFile, i as pluginCacheLstatSync, o as pluginCacheStatSync, r as pluginCacheExistsSync } from "./plugin-cache-files-DLPF_Tw2.js";
import { t as isBundledPluginInsideDevSourceRoot } from "./dev-source-root-DCu97Qvh.js";
import { l as normalizeManifestChannelCommandDefaults, n as isCoreReservedPluginId, r as loadPluginManifest } from "./manifest-ByRdkf9X.js";
import { t as normalizePluginPolicyId } from "./plugin-policy-id-4QxPdFqy.js";
import { o as resolveCompatibilityHostVersion } from "./version-v1kuAkGj.js";
import { a as resolvePluginActivationDecisionShared, n as normalizePluginsConfigWithResolverCore, o as toPluginActivationState, r as resolveChannelConfigEnablement, t as identityNormalizePluginId } from "./config-normalization-shared-D4lufwW0.js";
import { n as discoverOpenClawPlugins, p as resolvePluginCandidateInstallOwner, u as isPluginCandidateInstallOwnerAmbiguous } from "./discovery-D_VDsZuY.js";
import { o as loadBundleManifest } from "./bundle-manifest-C0Vv9-Hj.js";
import { t as shouldRejectHardlinkedPluginFiles } from "./hardlink-policy-CslDqyMD.js";
import { n as satisfiesPluginApiRange, t as resolvePackagePluginApiRange } from "./package-compat-JNCIUWrD.js";
import "./path-safety-BT3PFs5V.js";
import { a as getOfficialExternalPluginCatalogEntryForPackage, o as getOfficialExternalPluginCatalogManifest } from "./official-external-plugin-catalog-Dzu7dwBN.js";
import { d as resolveInstalledPluginIndexStorePath, i as loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader-SXWwf_BU.js";
import { t as checkMinHostVersion } from "./min-host-version-D3PR7DXa.js";
import { i as isTrustedOfficialPluginInstallRecord } from "./official-external-install-records-DClkub0e.js";
import path from "node:path";
//#region src/plugins/manifest-install-owner.ts
const PLUGIN_MANIFEST_INSTALL_OWNER = Symbol.for("openclaw.pluginManifestInstallOwner");
function recordPluginManifestInstallOwner(record, installOwner, ambiguous = false) {
if (!installOwner && !ambiguous) return record;
Object.defineProperty(record, PLUGIN_MANIFEST_INSTALL_OWNER, {
configurable: false,
enumerable: true,
value: ambiguous ? { ambiguous: true } : { installOwner }
});
return record;
}
function readPluginManifestInstallOwner(record) {
return record[PLUGIN_MANIFEST_INSTALL_OWNER];
}
function resolvePluginManifestInstallOwner(record) {
return readPluginManifestInstallOwner(record)?.installOwner;
}
function isPluginManifestInstallOwnerAmbiguous(record) {
return readPluginManifestInstallOwner(record)?.ambiguous === true;
}
//#endregion
//#region src/plugins/config-policy.ts
function normalizePluginsConfigWithResolver(config, normalizePluginId = identityNormalizePluginId) {
return normalizePluginsConfigWithResolverCore(config, normalizePluginId);
}
function resolvePolicyPluginActivationState(params) {
return toPluginActivationState(resolvePluginActivationDecisionShared({
...params,
activationSource: {
plugins: params.sourceConfig ?? params.config,
rootConfig: params.sourceRootConfig ?? params.rootConfig
},
resolveChannelConfigEnablement
}));
}
//#endregion
//#region src/plugins/manifest-registry.ts
function resolvePluginSourcePath(sourcePath) {
if (pluginCacheExistsSync(sourcePath)) return sourcePath;
if (sourcePath.endsWith(".ts")) {
const jsPath = sourcePath.slice(0, -3) + ".js";
if (pluginCacheExistsSync(jsPath)) return jsPath;
}
return sourcePath;
}
function isPluginRootPath(params) {
const resolvedTargetPath = path.resolve(params.targetPath);
const resolvedRootPath = path.resolve(params.rootPath);
if (!isPathInside(resolvedRootPath, resolvedTargetPath)) return false;
const targetRealPath = pluginCacheRealpathSync(resolvedTargetPath);
if (!targetRealPath) return params.targetMustExist !== true;
if (!isPathInside(params.rootRealPath, targetRealPath)) return false;
if (params.rejectHardlinks === true) {
const targetStat = pluginCacheStatSync(resolvedTargetPath);
if (!targetStat || targetStat.nlink > 1) return false;
}
return true;
}
function resolveManifestPluginSourcePath(params) {
const pushDiagnostic = () => {
params.diagnostics.push({
level: "warn",
pluginId: sanitizeForLog(params.pluginId),
source: sanitizeForLog(params.manifestPath),
message: `plugin manifest ${params.entryName} must resolve inside the plugin root; ignoring entry`
});
};
if (!params.entry || path.isAbsolute(params.entry)) {
pushDiagnostic();
return;
}
const rootPath = path.resolve(params.rootDir);
const rootRealPath = pluginCacheRealpathSync(rootPath) ?? rootPath;
const sourcePath = path.resolve(rootPath, params.entry);
if (!isPluginRootPath({
rootPath,
targetPath: sourcePath,
rootRealPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: pluginCacheExistsSync(sourcePath)
})) {
pushDiagnostic();
return;
}
const resolvedSourcePath = resolvePluginSourcePath(sourcePath);
if (!isPluginRootPath({
rootPath,
targetPath: resolvedSourcePath,
rootRealPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: pluginCacheExistsSync(resolvedSourcePath)
})) {
pushDiagnostic();
return;
}
return resolvedSourcePath;
}
const PORTABLE_PLUGIN_ICON_PATH = path.join("assets", "icon.png");
function resolvePortablePluginIconPath(params) {
const iconPath = path.resolve(params.rootDir, PORTABLE_PLUGIN_ICON_PATH);
const iconStat = pluginCacheLstatSync(iconPath);
if (!iconStat?.isFile() || params.rejectHardlinks && iconStat.nlink > 1) return;
const rootPath = path.resolve(params.rootDir);
return isPluginRootPath({
rootPath,
targetPath: iconPath,
rootRealPath: pluginCacheRealpathSync(rootPath) ?? rootPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: true
}) ? iconPath : void 0;
}
const PLUGIN_ORIGIN_RANK = {
config: 0,
workspace: 1,
global: 2,
bundled: 3
};
function rejectCaseFoldedIdCollisions(records, diagnostics) {
const recordsByPolicyId = /* @__PURE__ */ new Map();
for (const record of records) {
const policyId = normalizePluginPolicyId(record.id);
const matches = recordsByPolicyId.get(policyId) ?? [];
matches.push(record);
recordsByPolicyId.set(policyId, matches);
}
const rejected = /* @__PURE__ */ new Set();
for (const [policyId, matches] of recordsByPolicyId) {
const declaredIds = [...new Set(matches.map((record) => record.id))].toSorted();
if (declaredIds.length < 2) continue;
const message = `plugin ids ${declaredIds.map((id) => JSON.stringify(id)).join(", ")} collide as normalized id ${JSON.stringify(policyId)}; refusing all colliding plugins`;
for (const record of matches) {
rejected.add(record);
diagnostics.push({
level: "error",
pluginId: record.id,
source: record.source,
message
});
}
}
return records.filter((record) => !rejected.has(record));
}
function normalizePreferredPluginIds(raw) {
return normalizeOptionalTrimmedStringList(raw);
}
function mergePackageChannelMetaIntoChannelConfigs(params) {
const channelId = params.packageChannel?.id?.trim();
if (!channelId || isBlockedObjectKey(channelId) || !params.channelConfigs || !Object.hasOwn(params.channelConfigs, channelId)) return params.channelConfigs;
const existing = params.channelConfigs[channelId];
if (!existing) return params.channelConfigs;
const label = existing.label ?? normalizeOptionalString(params.packageChannel?.label) ?? "";
const description = existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? "";
const preferOver = existing.preferOver ?? normalizePreferredPluginIds(params.packageChannel?.preferOver);
const commands = existing.commands ?? normalizeManifestChannelCommandDefaults(params.packageChannel?.commands);
const merged = Object.create(null);
for (const [key, value] of Object.entries(params.channelConfigs)) if (!isBlockedObjectKey(key)) merged[key] = value;
merged[channelId] = {
...existing,
...label ? { label } : {},
...description ? { description } : {},
...preferOver?.length ? { preferOver } : {},
...commands ? { commands } : {}
};
return merged;
}
function mergeContractLists(left, right) {
const merged = uniqueStrings([...left ?? [], ...right ?? []].map((value) => value.trim()).filter((value) => value.length > 0));
return merged.length > 0 ? merged : void 0;
}
function mergeManifestContracts(manifestContracts, catalogContracts) {
if (!catalogContracts) return manifestContracts;
const contracts = {};
for (const key of [
"embeddedExtensionFactories",
"agentToolResultMiddleware",
"trustedToolPolicies",
"externalAuthProviders",
"embeddingProviders",
"speechProviders",
"realtimeTranscriptionProviders",
"realtimeVoiceProviders",
"mediaUnderstandingProviders",
"transcriptSourceProviders",
"documentExtractors",
"imageGenerationProviders",
"videoGenerationProviders",
"musicGenerationProviders",
"webContentExtractors",
"webFetchProviders",
"webSearchProviders",
"workerProviders",
"usageProviders",
"migrationProviders",
"gatewayMethodDispatch",
"tools"
]) {
const merged = mergeContractLists(manifestContracts?.[key], catalogContracts[key]);
if (merged) contracts[key] = merged;
}
return Object.keys(contracts).length > 0 ? contracts : void 0;
}
function mergeCatalogChannelConfigs(params) {
if (!params.catalogChannelConfigs) return params.manifestChannelConfigs;
const merged = Object.create(null);
for (const [key, value] of Object.entries(params.catalogChannelConfigs)) if (!isBlockedObjectKey(key)) merged[key] = value;
for (const [key, value] of Object.entries(params.manifestChannelConfigs ?? {})) if (!isBlockedObjectKey(key)) {
const catalogValue = merged[key];
merged[key] = catalogValue ? {
...catalogValue,
...value,
schema: value.schema ?? catalogValue.schema,
...catalogValue.uiHints || value.uiHints ? { uiHints: {
...catalogValue.uiHints,
...value.uiHints
} } : {},
...value.runtime ?? catalogValue.runtime ? { runtime: value.runtime ?? catalogValue.runtime } : {},
...value.label ?? catalogValue.label ? { label: value.label ?? catalogValue.label } : {},
...value.description ?? catalogValue.description ? { description: value.description ?? catalogValue.description } : {},
...value.preferOver ?? catalogValue.preferOver ? { preferOver: value.preferOver ?? catalogValue.preferOver } : {},
...value.commands ?? catalogValue.commands ? { commands: value.commands ?? catalogValue.commands } : {}
} : value;
}
return Object.keys(merged).length > 0 ? merged : void 0;
}
function mergeManifestCatalog(manifestCatalog, officialCatalog) {
const featuredCandidate = manifestCatalog?.featured ?? officialCatalog?.featured;
const orderCandidate = manifestCatalog?.order ?? officialCatalog?.order;
const featured = typeof featuredCandidate === "boolean" ? featuredCandidate : void 0;
const order = typeof orderCandidate === "number" && Number.isFinite(orderCandidate) ? orderCandidate : void 0;
if (featured === void 0 && order === void 0) return;
return {
...featured !== void 0 ? { featured } : {},
...order !== void 0 ? { order } : {}
};
}
function buildRecord(params) {
const pluginId = params.candidate.effectivePluginId ?? params.manifest.id;
const providerSourceEntry = params.manifest.providerCatalogEntry !== void 0 ? {
entryName: "providerCatalogEntry",
entry: params.manifest.providerCatalogEntry
} : void 0;
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 officialCatalogManifest = params.candidate.origin !== "bundled" ? getOfficialExternalPluginCatalogManifest(getOfficialExternalPluginCatalogEntryForPackage(params.candidate.packageName) ?? {}) : void 0;
const channelConfigs = mergePackageChannelMetaIntoChannelConfigs({
channelConfigs: mergeCatalogChannelConfigs({
manifestChannelConfigs,
catalogChannelConfigs: officialCatalogManifest?.channelConfigs
}),
packageChannel: params.candidate.packageManifest?.channel
});
const packageChannelCommands = normalizeManifestChannelCommandDefaults(params.candidate.packageManifest?.channel?.commands);
return {
id: pluginId,
backupResources: params.manifest.backupResources,
doctorContract: params.manifest.doctorContract,
doctorHealthChecks: params.manifest.doctorHealthChecks,
sessionRouteStateOwners: params.manifest.sessionRouteStateOwners,
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.packageName,
description: normalizeOptionalString(params.manifest.description) ?? params.candidate.packageDescription,
catalog: mergeManifestCatalog(params.manifest.catalog, officialCatalogManifest?.catalog),
iconPath: resolvePortablePluginIconPath({
rootDir: params.candidate.rootDir,
rejectHardlinks: params.rejectHardlinks
}),
version: normalizeOptionalString(params.manifest.version) ?? params.candidate.packageVersion,
packageName: params.candidate.packageName,
packageVersion: params.candidate.packageVersion,
packageDescription: params.candidate.packageDescription,
enabledByDefault: params.manifest.enabledByDefault === true ? true : void 0,
enabledByDefaultOnPlatforms: params.manifest.enabledByDefaultOnPlatforms,
autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders,
legacyPluginIds: params.manifest.legacyPluginIds,
format: params.candidate.format ?? "openclaw",
bundleFormat: params.candidate.bundleFormat,
kind: params.manifest.kind,
channels: params.manifest.channels ?? [],
providers: params.manifest.providers ?? [],
providerDiscoverySource: providerSourceEntry ? resolveManifestPluginSourcePath({
rootDir: params.candidate.rootDir,
manifestPath: params.manifestPath,
pluginId,
entryName: providerSourceEntry.entryName,
entry: providerSourceEntry.entry,
rejectHardlinks: params.rejectHardlinks,
diagnostics: params.diagnostics
}) : void 0,
capabilityCatalogSource: params.manifest.capabilityCatalogEntry === void 0 ? void 0 : resolveManifestPluginSourcePath({
rootDir: params.candidate.rootDir,
manifestPath: params.manifestPath,
pluginId,
entryName: "capabilityCatalogEntry",
entry: params.manifest.capabilityCatalogEntry,
rejectHardlinks: params.rejectHardlinks,
diagnostics: params.diagnostics
}) ?? null,
modelSupport: params.manifest.modelSupport,
modelCatalog: params.manifest.modelCatalog,
modelPricing: params.manifest.modelPricing,
modelIdNormalization: params.manifest.modelIdNormalization,
providerEndpoints: params.manifest.providerEndpoints,
providerRequest: params.manifest.providerRequest,
secretProviderIntegrations: params.manifest.secretProviderIntegrations,
cliBackends: params.manifest.cliBackends ?? [],
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [],
commandAliases: params.manifest.commandAliases,
cliCommands: params.manifest.cliCommands,
providerUsageAuthEnvVars: params.manifest.providerUsageAuthEnvVars,
providerAuthAliases: params.manifest.providerAuthAliases,
providerAuthChoices: params.manifest.providerAuthChoices,
activation: params.manifest.activation,
setup: params.manifest.setup,
packageManifest: params.candidate.packageManifest,
packageDependencies: params.candidate.packageDependencies,
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
packageChannel: params.candidate.packageManifest?.channel,
packageInstall: params.candidate.packageManifest?.install,
trustedOfficialInstall: params.trust.reason === "trusted-official" ? true : void 0,
trust: params.trust,
qaRunners: params.manifest.qaRunners,
dashboard: params.manifest.dashboard,
controlUi: params.manifest.controlUi,
mcpServers: params.manifest.mcpServers,
skills: params.manifest.skills ?? [],
settingsFiles: [],
hooks: [],
origin: params.candidate.origin,
workspaceDir: params.candidate.workspaceDir,
rootDir: params.candidate.rootDir,
source: params.candidate.source,
setupSource: params.candidate.setupSource,
manifestPath: params.manifestPath,
schemaCacheKey: params.schemaCacheKey,
configSchema: params.configSchema,
configUiHints: params.manifest.uiHints,
contracts: mergeManifestContracts(params.manifest.contracts, officialCatalogManifest?.contracts),
mediaUnderstandingProviderMetadata: params.manifest.mediaUnderstandingProviderMetadata,
imageGenerationProviderMetadata: params.manifest.imageGenerationProviderMetadata,
videoGenerationProviderMetadata: params.manifest.videoGenerationProviderMetadata,
musicGenerationProviderMetadata: params.manifest.musicGenerationProviderMetadata,
toolMetadata: params.manifest.toolMetadata,
configContracts: params.manifest.configContracts,
channelConfigs,
...params.candidate.packageManifest?.channel?.id ? { channelCatalogMeta: {
id: params.candidate.packageManifest.channel.id,
...typeof params.candidate.packageManifest.channel.label === "string" ? { label: params.candidate.packageManifest.channel.label } : {},
...typeof params.candidate.packageManifest.channel.blurb === "string" ? { blurb: params.candidate.packageManifest.channel.blurb } : {},
...params.candidate.packageManifest.channel.preferOver ? { preferOver: params.candidate.packageManifest.channel.preferOver } : {},
...packageChannelCommands ? { commands: packageChannelCommands } : {}
} } : {}
};
}
function buildBundleRecord(params) {
return {
id: params.manifest.id,
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.idHint,
description: normalizeOptionalString(params.manifest.description),
iconPath: resolvePortablePluginIconPath({
rootDir: params.candidate.rootDir,
rejectHardlinks: params.rejectHardlinks
}),
version: normalizeOptionalString(params.manifest.version),
packageName: params.candidate.packageName,
packageVersion: params.candidate.packageVersion,
packageDescription: params.candidate.packageDescription,
packageManifest: params.candidate.packageManifest,
packageDependencies: params.candidate.packageDependencies,
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
packageChannel: params.candidate.packageManifest?.channel,
packageInstall: params.candidate.packageManifest?.install,
format: "bundle",
bundleFormat: params.candidate.bundleFormat,
bundleCapabilities: params.manifest.capabilities,
activation: params.manifest.activation,
channels: [],
providers: [],
cliBackends: [],
syntheticAuthRefs: [],
nonSecretAuthMarkers: [],
skills: params.manifest.skills ?? [],
settingsFiles: params.manifest.settingsFiles ?? [],
hooks: params.manifest.hooks ?? [],
origin: params.candidate.origin,
workspaceDir: params.candidate.workspaceDir,
rootDir: params.candidate.rootDir,
source: params.candidate.source,
manifestPath: params.manifestPath,
schemaCacheKey: void 0,
configSchema: void 0,
configUiHints: void 0,
configContracts: void 0,
channelConfigs: void 0
};
}
function pushNonBundledChannelConfigDescriptorDiagnostic(params) {
if (params.record.origin === "bundled" || params.record.format === "bundle") return;
const configuredEntry = params.normalized?.entries[params.record.id];
if (params.normalized?.enabled === false || configuredEntry?.enabled === false || params.normalized?.deny.includes(params.record.id) || params.normalized?.allow.length && !params.normalized.allow.includes(params.record.id)) return;
const declaredChannels = params.record.channels.map((channelId) => channelId.trim()).filter((channelId) => channelId.length > 0);
if (declaredChannels.length === 0) return;
const channelConfigs = params.record.channelConfigs ?? {};
const missingChannels = declaredChannels.filter((channelId) => !Object.hasOwn(channelConfigs, channelId));
if (missingChannels.length === 0) return;
const safeMissingChannels = missingChannels.map(sanitizeForLog);
params.diagnostics.push({
level: "warn",
pluginId: sanitizeForLog(params.record.id),
source: sanitizeForLog(params.record.manifestPath),
message: `channel plugin manifest declares ${safeMissingChannels.join(", ")} without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads. Channels without channelConfigs still appear in channel listings, but setup UI may be limited.`
});
}
function pushManifestCompatibilityDiagnostics(params) {
pushNonBundledChannelConfigDescriptorDiagnostic(params);
}
function dedupePluginDiagnostics(diagnostics, discoveryDiagnostics) {
const seen = /* @__PURE__ */ new Set();
const deduped = [];
for (const diagnostic of diagnostics) {
const key = JSON.stringify([
diagnostic.level,
diagnostic.pluginId ?? "",
diagnostic.message,
diagnostic.level === "error" || discoveryDiagnostics.has(diagnostic) ? diagnostic.source ?? "" : ""
]);
if (seen.has(key)) continue;
seen.add(key);
deduped.push(diagnostic);
}
return deduped;
}
function resolveCandidateInstallOwner(params) {
if (isPluginCandidateInstallOwnerAmbiguous(params.candidate)) return;
const installOwner = resolvePluginCandidateInstallOwner(params.candidate);
if (installOwner) return Object.hasOwn(params.installRecords, installOwner) ? installOwner : void 0;
}
function matchesInstalledPluginRecord(params) {
if (params.candidate.origin !== "global" && params.candidate.origin !== "config") return false;
const installOwner = resolveCandidateInstallOwner(params);
const record = installOwner ? params.installRecords[installOwner] : void 0;
if (!record) return false;
const candidatePaths = [
params.candidate.rootDir,
params.candidate.packageDir,
params.candidate.source,
params.candidate.setupSource
].filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => {
const resolved = resolveUserPath(entry, params.env);
return pluginCacheRealpathSync(resolved) ?? resolved;
});
const trackedPaths = (params.installPathOnly ? [record.installPath] : [record.installPath, record.sourcePath]).filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => {
const resolved = resolveUserPath(entry, params.env);
return pluginCacheRealpathSync(resolved) ?? resolved;
});
if (candidatePaths.length === 0 || trackedPaths.length === 0) return false;
return trackedPaths.some((trackedPath) => candidatePaths.some((candidatePath) => candidatePath === trackedPath || isPathInside(trackedPath, candidatePath) || isPathInside(candidatePath, trackedPath)));
}
function resolvePluginTrust(params) {
const installOwner = resolveCandidateInstallOwner(params);
const record = installOwner ? params.installRecords[installOwner] : void 0;
const origin = params.candidate.origin;
let reason;
if (origin === "bundled") reason = "bundled";
else if (isPluginCandidateInstallOwnerAmbiguous(params.candidate)) reason = "owner-ambiguous";
else if (origin === "workspace" || record?.source === "path" || record?.source === "npm" && (record.artifactKind !== void 0 || record.sourcePath !== void 0)) reason = "origin-path";
else if (!record || !installOwner) reason = "record-missing";
else if (!matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.candidate,
env: params.env,
installRecords: params.installRecords,
installPathOnly: true
})) reason = "install-path-mismatch";
else if (isTrustedOfficialPluginInstallRecord({
pluginId: installOwner,
packageName: params.candidate.packageName,
record
})) reason = "trusted-official";
else if (record.source === "npm" && record.spec === void 0 && record.resolvedName === void 0 && record.resolvedSpec === void 0 || record.source === "clawhub" && record.clawhubUrl === void 0 && record.clawhubChannel === void 0) reason = "provenance-missing";
else reason = "provenance-invalid";
return {
reason,
registryPath: params.registryPath,
origin,
installSource: record?.source,
installSpec: record?.spec === void 0 ? void 0 : redactSensitiveText(record.spec, { mode: "tools" })
};
}
function resolveDuplicatePrecedenceRank(params) {
if (params.candidate.origin === "config" || params.candidate.configSelected) return 0;
if (params.candidate.origin === "bundled" && isBundledPluginInsideDevSourceRoot({
rootDir: params.candidate.rootDir,
env: params.env
})) return 1;
if (params.candidate.origin === "global" && matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.candidate,
config: params.config,
env: params.env,
installRecords: params.installRecords
})) return 2;
if (params.candidate.origin === "bundled") return 3;
if (params.candidate.origin === "workspace") return 4;
return 5;
}
function isIntentionalInstalledBundledDuplicate(params) {
const leftIsInstalled = matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.left,
config: params.config,
env: params.env,
installRecords: params.installRecords
});
const rightIsInstalled = matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.right,
config: params.config,
env: params.env,
installRecords: params.installRecords
});
return leftIsInstalled && params.right.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({
rootDir: params.right.rootDir,
env: params.env
}) || rightIsInstalled && params.left.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({
rootDir: params.left.rootDir,
env: params.env
});
}
function isSameGlobalPackageDuplicate(left, right) {
if (left.origin !== "global" || right.origin !== "global") return false;
const leftPackageName = normalizeOptionalString(left.packageName);
const rightPackageName = normalizeOptionalString(right.packageName);
if (!leftPackageName || leftPackageName !== rightPackageName) return false;
const leftPackageVersion = normalizeOptionalString(left.packageVersion);
const rightPackageVersion = normalizeOptionalString(right.packageVersion);
return Boolean(leftPackageVersion && rightPackageVersion && leftPackageVersion === rightPackageVersion);
}
function loadPluginManifestRegistryCore(params = {}) {
if (!params.candidates && !params.discovery && !params.installRecords) {
const gatewaySnapshot = getGatewayPluginMetadataSnapshot();
if (gatewaySnapshot) return gatewaySnapshot.manifestRegistry;
}
const config = params.config ?? {};
const normalized = normalizePluginsConfigWithResolver(config.plugins);
const env = params.env ?? process.env;
const registryPath = params.registryPath ?? resolveInstalledPluginIndexStorePath({ env });
let installRecords = params.installRecords;
let installRecordsLoaded = Boolean(params.installRecords);
const getInstallRecords = () => {
if (!installRecordsLoaded) {
installRecords = loadInstalledPluginIndexInstallRecordsSync({ env });
installRecordsLoaded = true;
}
return installRecords ?? {};
};
const discovery = params.candidates ? {
candidates: params.candidates,
diagnostics: params.diagnostics ?? []
} : params.discovery ?? discoverOpenClawPlugins({
workspaceDir: params.workspaceDir,
extraPaths: normalized.loadPaths,
env,
installRecords: getInstallRecords()
});
const discovered = new Set(discovery.diagnostics);
const diagnostics = [...discovered];
const candidates = discovery.candidates;
const seenIds = /* @__PURE__ */ new Map();
const currentHostVersion = resolveCompatibilityHostVersion(env);
const explicitConfiguredFileSources = new Set(normalized.loadPaths.map((loadPath) => resolveUserPath(loadPath, env)).filter((loadPath) => pluginCacheStatSync(loadPath)?.isFile() === true).map((loadPath) => path.resolve(loadPath)));
for (const candidate of candidates) {
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: candidate.origin,
rootDir: candidate.rootDir,
env
});
const isBundleRecord = (candidate.format ?? "openclaw") === "bundle";
const isManifestlessConfiguredFile = candidate.origin === "config" && explicitConfiguredFileSources.has(path.resolve(candidate.source)) && !pluginCacheExistsSync(path.join(candidate.rootDir, "openclaw.plugin.json"));
if (isManifestlessConfiguredFile && isCoreReservedPluginId(candidate.idHint)) {
diagnostics.push({
level: "error",
pluginId: candidate.idHint,
source: candidate.source,
message: `plugin manifest id "${candidate.idHint}" is reserved by OpenClaw core`
});
continue;
}
const manifestRes = candidate.origin === "bundled" && candidate.bundledManifest && candidate.bundledManifestPath ? {
ok: true,
manifest: candidate.bundledManifest,
manifestPath: candidate.bundledManifestPath
} : isBundleRecord && candidate.bundleFormat ? loadBundleManifest({
rootDir: candidate.rootDir,
bundleFormat: candidate.bundleFormat,
rejectHardlinks
}) : isManifestlessConfiguredFile ? {
ok: true,
manifest: {
id: candidate.idHint,
configSchema: {
type: "object",
additionalProperties: false
}
},
manifestPath: candidate.source
} : loadPluginManifest(candidate.rootDir, rejectHardlinks);
if (!manifestRes.ok) {
diagnostics.push({
level: "error",
pluginId: candidate.diagnosticIdHint ?? candidate.idHint,
message: manifestRes.error,
source: manifestRes.manifestPath,
..."diagnosticCode" in manifestRes && manifestRes.diagnosticCode ? { code: manifestRes.diagnosticCode } : {}
});
continue;
}
const manifest = manifestRes.manifest;
const effectivePluginId = candidate.effectivePluginId ?? manifest.id;
if (candidate.origin !== "bundled") {
const packageManifestSource = path.join(candidate.packageDir ?? candidate.rootDir, "package.json");
const allowLegacyBareMinHostVersion = candidate.origin === "global" && matchesInstalledPluginRecord({
pluginId: effectivePluginId,
candidate,
config,
env,
installRecords: getInstallRecords()
});
const minHostVersionCheck = checkMinHostVersion({
currentVersion: currentHostVersion,
minHostVersion: candidate.packageManifest?.install?.minHostVersion,
allowLegacyBareSemver: allowLegacyBareMinHostVersion
});
if (!minHostVersionCheck.ok) {
diagnostics.push({
level: minHostVersionCheck.kind === "invalid" ? "error" : "warn",
pluginId: effectivePluginId,
source: packageManifestSource,
message: minHostVersionCheck.kind === "invalid" ? `plugin manifest invalid | ${minHostVersionCheck.error}` : minHostVersionCheck.kind === "unknown_host_version" ? `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host version could not be determined; skipping load` : `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host is ${minHostVersionCheck.currentVersion}; skipping load`
});
continue;
}
const packagePluginApiRangeCheck = resolvePackagePluginApiRange(candidate.packageManifest);
if (!packagePluginApiRangeCheck.ok) {
diagnostics.push({
level: "error",
pluginId: effectivePluginId,
source: packageManifestSource,
message: `plugin manifest invalid | ${packagePluginApiRangeCheck.error}`
});
continue;
}
const packagePluginApiRange = packagePluginApiRangeCheck.range;
if (packagePluginApiRange && !satisfiesPluginApiRange(currentHostVersion, packagePluginApiRange)) {
diagnostics.push({
level: "warn",
pluginId: effectivePluginId,
source: packageManifestSource,
message: `plugin requires plugin API ${packagePluginApiRange}, but this host is ${currentHostVersion}; skipping load (check "openclaw --version", OPENCLAW_COMPATIBILITY_HOST_VERSION, or run "openclaw doctor")`
});
continue;
}
}
const configSchema = "configSchema" in manifest ? manifest.configSchema : void 0;
const schemaCacheKey = (() => {
if (!configSchema || isManifestlessConfiguredFile) return;
const file = readPluginCacheFile({
rootDir: candidate.rootDir,
relativePath: path.relative(candidate.rootDir, manifestRes.manifestPath),
rejectHardlinks,
maxBytes: 262144
});
return file.ok ? `${manifestRes.manifestPath}:${file.hash}` : manifestRes.manifestPath;
})();
const record = isBundleRecord ? buildBundleRecord({
manifest,
candidate,
manifestPath: manifestRes.manifestPath,
rejectHardlinks
}) : buildRecord({
manifest,
candidate,
manifestPath: manifestRes.manifestPath,
diagnostics,
rejectHardlinks,
schemaCacheKey,
configSchema,
trust: resolvePluginTrust({
registryPath,
pluginId: effectivePluginId,
candidate,
env,
installRecords: getInstallRecords()
}),
...params.bundledChannelConfigCollector ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } : {}
});
if (candidate.sourcePreferred || candidate.origin === "bundled" && candidate.configSelected) record.sourcePreferred = true;
recordPluginManifestInstallOwner(record, resolvePluginCandidateInstallOwner(candidate), isPluginCandidateInstallOwnerAmbiguous(candidate));
const existing = seenIds.get(effectivePluginId);
if (existing) {
const samePath = existing.candidate.rootDir === candidate.rootDir;
if ((() => {
if (samePath) return true;
const existingReal = pluginCacheRealpathSync(existing.candidate.rootDir);
const candidateReal = pluginCacheRealpathSync(candidate.rootDir);
return Boolean(existingReal && candidateReal && existingReal === candidateReal);
})()) {
if (record.sourcePreferred || existing.record.sourcePreferred) {
record.sourcePreferred = true;
existing.record.sourcePreferred = true;
}
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
seenIds.set(effectivePluginId, {
candidate,
record
});
pushManifestCompatibilityDiagnostics({
record,
diagnostics,
normalized
});
}
continue;
}
const candidateWins = resolveDuplicatePrecedenceRank({
pluginId: effectivePluginId,
candidate,
config,
env,
installRecords: getInstallRecords()
}) < resolveDuplicatePrecedenceRank({
pluginId: effectivePluginId,
candidate: existing.candidate,
config,
env,
installRecords: getInstallRecords()
});
const winnerCandidate = candidateWins ? candidate : existing.candidate;
const overriddenCandidate = candidateWins ? existing.candidate : candidate;
if (candidateWins) {
seenIds.set(effectivePluginId, {
candidate,
record
});
pushManifestCompatibilityDiagnostics({
record,
diagnostics,
normalized
});
}
if (isIntentionalInstalledBundledDuplicate({
pluginId: effectivePluginId,
left: candidate,
right: existing.candidate,
config,
env,
installRecords: getInstallRecords()
})) continue;
if (isSameGlobalPackageDuplicate(candidate, existing.candidate)) continue;
diagnostics.push({
level: "warn",
pluginId: effectivePluginId,
source: overriddenCandidate.source,
message: winnerCandidate.origin === "config" ? `duplicate plugin id resolved by explicit config-selected plugin; ${overriddenCandidate.origin} plugin will be overridden by config plugin (${winnerCandidate.source})` : `duplicate plugin id detected; ${overriddenCandidate.origin} plugin will be overridden by ${winnerCandidate.origin} plugin (${winnerCandidate.source})`
});
continue;
}
seenIds.set(effectivePluginId, {
candidate,
record
});
pushManifestCompatibilityDiagnostics({
record,
diagnostics,
normalized
});
}
return {
plugins: rejectCaseFoldedIdCollisions([...seenIds.values()].map(({ record }) => record), diagnostics),
diagnostics: dedupePluginDiagnostics(diagnostics, discovered)
};
}
/** Load manifest metadata from the bundled/source plugin tree without consulting operator state. */
function loadBundledPluginManifestRegistry(params = {}) {
const env = params.env ?? process.env;
const installRecords = {};
return loadPluginManifestRegistryCore({
env,
installRecords,
discovery: discoverOpenClawPlugins({
env,
installRecords,
rootScope: "bundled"
})
});
}
//#endregion
export { isPluginManifestInstallOwnerAmbiguous as a, resolvePolicyPluginActivationState as i, loadPluginManifestRegistryCore as n, resolvePluginManifestInstallOwner as o, normalizePluginsConfigWithResolver as r, loadBundledPluginManifestRegistry as t };