import type { GatewayRequestHandler } from "../gateway/server-methods/types.js"; import { normalizeAgentToolResultMiddlewareRuntimeIds } from "./agent-tool-result-middleware.js"; import { recordPluginInstallOwnerLookup, resolvePluginCandidateInstallOwner, } from "./candidate-install-owner.js"; import { resolveEffectivePluginActivationState } from "./config-state.js"; import { isPluginEnabledByDefaultForPlatform } from "./default-enablement.js"; import { getReusableCachedPluginRegistry, pluginLoaderCacheState, setCachedPluginRegistry, } from "./loader-cache.js"; import { resolvePluginLoadDiscovery } from "./loader-discovery.js"; import { resolvePluginLoadCacheContext, resolveRuntimeSubagentMode, } from "./loader-load-context.js"; import { createLazyPluginRuntime, createPluginModuleLoader } from "./loader-module-runtime.js"; import { warnAboutUntrackedLoadedPlugins } from "./loader-provenance.js"; import { formatPluginFailureSummary } from "./loader-records.js"; import { loadRuntimePluginCandidate, type PluginLoadLoopState, } from "./loader-runtime-candidate.js"; import { activatePluginRegistry, createPluginLoaderLogger, maybeThrowOnPluginLoadError, resolveAuthorizedDreamingSidecar, } from "./loader-shared.js"; import type { PluginLoadOptions } from "./loader-types.js"; import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import { getPluginRegistryRuntime } from "./registry-runtime-binding.js"; import { createPluginRegistry, type PluginRegistry } from "./registry.js"; import { getActivePluginRegistry } from "./runtime.js"; import type { PluginRuntime } from "./runtime/types.js"; type PluginModuleLoaderOverrides = Pick< Parameters[0], "tryNative" | "loaderFilename" | "installNativeSdkResolver" >; type InternalPluginLoadOverrides = { moduleLoader: PluginModuleLoaderOverrides; runtime: Pick; }; function createDeferredGatewaySubagentRuntime(runtime: PluginRuntime): PluginRuntime["subagent"] { return { run: (...args) => runtime.subagent.run(...args), waitForRun: (...args) => runtime.subagent.waitForRun(...args), getSessionMessages: (...args) => runtime.subagent.getSessionMessages(...args), deleteSession: (...args) => runtime.subagent.deleteSession(...args), }; } function createDeferredGatewayNodesRuntime(runtime: PluginRuntime): PluginRuntime["nodes"] { return { list: (...args) => runtime.nodes.list(...args), invoke: (...args) => runtime.nodes.invoke(...args), }; } export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry { return loadOpenClawPluginsInternal(options); } /** Internal entry for host-owned snapshots that need a narrow registration runtime. */ export function loadOpenClawPluginsWithInternalOverrides( options: PluginLoadOptions & { cache: false }, overrides: InternalPluginLoadOverrides, ): PluginRegistry { return loadOpenClawPluginsInternal(options, overrides); } function loadOpenClawPluginsInternal( options: PluginLoadOptions, overrides?: InternalPluginLoadOverrides, ): PluginRegistry { const requestedOnlyPluginIds = normalizePluginIdScope(options.onlyPluginIds); const requestedOnlyPluginIdSet = createPluginIdScopeSet(requestedOnlyPluginIds); if (requestedOnlyPluginIdSet && requestedOnlyPluginIdSet.size === 0) { const emptyRegistry = createEmptyPluginRegistry(); if (options.activate !== false) { const runtimeSubagentMode = resolveRuntimeSubagentMode(options.runtimeOptions); activatePluginRegistry( emptyRegistry, `empty-plugin-scope::${runtimeSubagentMode}::${options.workspaceDir ?? ""}`, runtimeSubagentMode, options.workspaceDir, ); } return emptyRegistry; } const context = resolvePluginLoadCacheContext(options); const logger = options.logger ?? createPluginLoaderLogger(); const validateOnly = options.mode === "validate"; const onlyPluginIdSet = createPluginIdScopeSet(context.onlyPluginIds); const cacheEnabled = options.cache !== false && options.resolveRawConfigEnvVars !== true; if (cacheEnabled) { const cached = getReusableCachedPluginRegistry(context.cacheKey); if (cached) { if (context.shouldActivate) { activatePluginRegistry( cached, context.cacheKey, context.runtimeSubagentMode, options.workspaceDir, ); } return cached; } } pluginLoaderCacheState.beginLoad(context.cacheKey); let registryBuilder: ReturnType | undefined; try { // Module and runtime loading stay lazy for discovery-only or disabled-plugin paths. const loadPluginModule = createPluginModuleLoader({ devSourceRoot: context.devSourceRoot, pluginSdkResolution: options.pluginSdkResolution, ...overrides?.moduleLoader, }); const activeRuntime = options.runtimeOptions?.allowGatewaySubagentBinding === true ? getActivePluginRegistry() : undefined; const activeGatewayRuntime = activeRuntime ? getPluginRegistryRuntime(activeRuntime) : undefined; const borrowedSubagent = activeGatewayRuntime ? createDeferredGatewaySubagentRuntime(activeGatewayRuntime) : undefined; const borrowedNodes = activeGatewayRuntime ? createDeferredGatewayNodesRuntime(activeGatewayRuntime) : undefined; const runtime = overrides?.runtime ? // The registry wraps this discovery-only base with scoped lazy capabilities. (overrides.runtime as unknown as PluginRuntime) : createLazyPluginRuntime({ devSourceRoot: context.devSourceRoot, pluginSdkResolution: options.pluginSdkResolution, runtimeOptions: { ...options.runtimeOptions, subagent: options.runtimeOptions?.subagent ?? borrowedSubagent, nodes: options.runtimeOptions?.nodes ?? borrowedNodes, }, loadPluginModule, }); registryBuilder = createPluginRegistry({ logger, runtime, coreGatewayHandlers: options.coreGatewayHandlers as Record, ...(options.coreGatewayMethodNames !== undefined && { coreGatewayMethodNames: options.coreGatewayMethodNames, }), ...(options.hostServices !== undefined && { hostServices: options.hostServices }), activateGlobalSideEffects: context.shouldActivate, }); const { registry } = registryBuilder; const { manifestRegistry, orderedCandidates, manifestBySource, provenance } = resolvePluginLoadDiscovery({ options, context, diagnostics: registry.diagnostics, logger, onlyPluginIdSet, emitWarning: context.shouldActivate, warningCacheKey: context.cacheKey, suppliedManifestRegistry: options.manifestRegistry, }); const selectedMiddlewareOwnerManifests = new Map< string, (typeof manifestRegistry.plugins)[number] >(); for (const candidate of orderedCandidates) { const record = manifestBySource.get(candidate.source); if (record && !selectedMiddlewareOwnerManifests.has(record.id)) { selectedMiddlewareOwnerManifests.set(record.id, record); } } for (const record of selectedMiddlewareOwnerManifests.values()) { const activation = resolveEffectivePluginActivationState({ id: record.id, origin: record.origin, config: context.normalized, rootConfig: context.cfg, enabledByDefault: isPluginEnabledByDefaultForPlatform(record), activationSource: context.activationSource, }); const runtimes = normalizeAgentToolResultMiddlewareRuntimeIds( record.contracts?.agentToolResultMiddleware, ); if ( runtimes.length > 0 && (record.origin === "bundled" || (activation.enabled && activation.explicitlyEnabled)) ) { registry.agentToolResultMiddlewareOwners.push({ pluginId: record.id, runtimes, manifest: record, }); } } const memorySlot = context.normalized.slots.memory; const state: PluginLoadLoopState = { seenIds: new Map(), selectedMemoryPluginId: null, memorySlotMatched: false, pluginLoadAttemptCount: 0, }; const dreamingSidecar = resolveAuthorizedDreamingSidecar({ cfg: context.cfg, normalized: context.normalized, activationSource: context.activationSource, manifestRegistry, memorySlot, }); const pluginLoadStartMs = performance.now(); for (const candidate of orderedCandidates) { const manifestRecord = manifestBySource.get(candidate.source); if (!manifestRecord) { continue; } loadRuntimePluginCandidate({ candidate, manifestRecord, context, options, onlyPluginIdSet, dreamingSidecar, validateOnly, registryBuilder, loadPluginModule, logger, state, }); } const pluginLoadElapsedMs = performance.now() - pluginLoadStartMs; if (state.pluginLoadAttemptCount > 0) { logger.debug?.( `[plugins] loaded ${registry.plugins.length} plugin(s) (${state.pluginLoadAttemptCount} attempted) in ${pluginLoadElapsedMs.toFixed(1)}ms`, ); } // Scoped snapshots may omit the configured memory plugin intentionally. if (!onlyPluginIdSet && typeof memorySlot === "string" && !state.memorySlotMatched) { registry.diagnostics.push({ level: "warn", message: `memory slot plugin not found or not marked as memory: ${memorySlot}`, }); } warnAboutUntrackedLoadedPlugins( recordPluginInstallOwnerLookup( { registry, provenance, allowlist: context.normalized.allow, emitWarning: context.shouldActivate, logger, env: context.env, }, new Map( orderedCandidates.flatMap((candidate) => { const pluginId = manifestBySource.get(candidate.source)?.id; const installOwner = resolvePluginCandidateInstallOwner(candidate); return pluginId && installOwner ? [[pluginId, installOwner] as const] : []; }), ), ), ); maybeThrowOnPluginLoadError(registry, options.throwOnLoadError); if (context.shouldActivate && options.mode !== "validate") { const failedPlugins = registry.plugins.filter((plugin) => plugin.failedAt != null); if (failedPlugins.length > 0) { logger.warn( `[plugins] ${failedPlugins.length} plugin(s) failed to initialize (${formatPluginFailureSummary( failedPlugins, )}). Run 'openclaw plugins inspect --runtime --json' for runtime diagnostics, 'openclaw plugins list' for registry state, and restart the Gateway after plugin code or load-path changes.`, ); } } if (context.shouldActivate) { // Install the complete bundle before hook-runner initialization. activatePluginRegistry( registry, context.cacheKey, context.runtimeSubagentMode, options.workspaceDir, ); } // Publish only complete registries: failed activation restores the prior runtime selection, // then the catch below can discard this builder without poisoning a reusable cache value. if (cacheEnabled) { setCachedPluginRegistry(context.cacheKey, registry); } return registry; } catch (error) { // Registration failures discard only an inactive builder. Activation is failure-atomic, and // any later cache failure must not strip the registry already serving runtime consumers. if (context.shouldActivate && registryBuilder?.registry !== getActivePluginRegistry()) { for (const plugin of registryBuilder?.registry.plugins.toReversed() ?? []) { if (plugin.status === "loaded") { registryBuilder?.rollbackPluginGlobalSideEffects(plugin.id); } } } throw error; } finally { pluginLoaderCacheState.finishLoad(context.cacheKey); } }