2026-04-02 20:57:14 +09:00
import { createHash } from "node:crypto" ;
2026-01-18 02:51:42 +00:00
import fs from "node:fs" ;
import path from "node:path" ;
2026-02-18 01:34:35 +00:00
import { createJiti } from "jiti" ;
2026-03-15 18:46:22 -07:00
import type { ChannelPlugin } from "../channels/plugins/types.js" ;
2026-04-04 09:18:28 +01:00
import { isChannelConfigured } from "../config/channel-configured.js" ;
2026-01-30 03:15:10 +01:00
import type { OpenClawConfig } from "../config/config.js" ;
2026-03-12 15:31:31 +00:00
import type { PluginInstallRecord } from "../config/types.plugins.js" ;
2026-01-11 12:11:12 +00:00
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js" ;
2026-02-26 13:04:33 +01:00
import { openBoundaryFileSync } from "../infra/boundary-file-read.js" ;
2026-01-18 23:24:42 +00:00
import { createSubsystemLogger } from "../logging/subsystem.js" ;
2026-01-11 12:11:12 +00:00
import { resolveUserPath } from "../utils.js" ;
2026-03-29 18:49:57 -04:00
import { buildPluginApi } from "./api-builder.js" ;
2026-03-16 21:46:05 -07:00
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js" ;
2026-03-22 16:32:17 +00:00
import { clearPluginCommands } from "./command-registry-state.js" ;
2026-01-19 21:13:51 -06:00
import {
2026-02-07 07:57:50 +00:00
applyTestPluginDefaults ,
2026-04-03 23:36:52 +09:00
createPluginActivationSource ,
2026-01-19 21:13:51 -06:00
normalizePluginsConfig ,
2026-02-23 19:40:32 +00:00
resolveEffectiveEnableState ,
2026-04-02 20:57:14 +09:00
resolveEffectivePluginActivationState ,
2026-01-19 21:13:51 -06:00
resolveMemorySlotDecision ,
2026-04-03 23:36:52 +09:00
type PluginActivationConfigSource ,
2026-01-19 21:13:51 -06:00
type NormalizedPluginsConfig ,
2026-04-02 20:57:14 +09:00
type PluginActivationState ,
2026-01-19 21:13:51 -06:00
} from "./config-state.js" ;
2026-02-01 10:03:47 +09:00
import { discoverOpenClawPlugins } from "./discovery.js" ;
2026-01-18 05:40:58 +00:00
import { initializeGlobalHookRunner } from "./hook-runner-global.js" ;
2026-04-02 13:54:53 +01:00
import { clearPluginInteractiveHandlers } from "./interactive-registry.js" ;
2026-02-01 10:03:47 +09:00
import { loadPluginManifestRegistry } from "./manifest-registry.js" ;
2026-04-05 14:43:29 +01:00
import type { PluginManifestContracts } from "./manifest.js" ;
2026-03-27 00:40:45 +00:00
import {
clearMemoryEmbeddingProviders ,
2026-03-27 02:01:07 +00:00
listRegisteredMemoryEmbeddingProviders ,
restoreRegisteredMemoryEmbeddingProviders ,
2026-03-27 00:40:45 +00:00
} from "./memory-embedding-providers.js" ;
2026-03-26 22:14:33 +00:00
import {
clearMemoryPluginState ,
getMemoryFlushPlanResolver ,
getMemoryPromptSectionBuilder ,
2026-03-26 22:29:38 +00:00
getMemoryRuntime ,
2026-03-26 22:14:33 +00:00
restoreMemoryPluginState ,
} from "./memory-state.js" ;
2026-02-19 15:24:02 +01:00
import { isPathInside , safeStatSync } from "./path-safety.js" ;
2026-01-14 14:31:43 +00:00
import { createPluginRegistry , type PluginRecord , type PluginRegistry } from "./registry.js" ;
2026-03-12 15:31:31 +00:00
import { resolvePluginCacheInputs } from "./roots.js" ;
2026-03-28 00:06:52 -04:00
import {
getActivePluginRegistry ,
getActivePluginRegistryKey ,
2026-04-02 22:50:17 +09:00
recordImportedPluginId ,
2026-03-28 00:06:52 -04:00
setActivePluginRegistry ,
} from "./runtime.js" ;
2026-03-15 20:02:24 -07:00
import type { CreatePluginRuntimeOptions } from "./runtime/index.js" ;
2026-03-04 02:58:48 +02:00
import type { PluginRuntime } from "./runtime/types.js" ;
2026-01-19 21:13:51 -06:00
import { validateJsonSchemaValue } from "./schema-validator.js" ;
2026-03-19 03:12:16 +00:00
import {
2026-03-21 20:17:47 +00:00
buildPluginLoaderAliasMap ,
2026-03-19 03:12:16 +00:00
buildPluginLoaderJitiOptions ,
listPluginSdkAliasCandidates ,
listPluginSdkExportedSubpaths ,
2026-03-27 13:38:40 +00:00
type PluginSdkResolutionPreference ,
2026-03-21 20:17:47 +00:00
resolveExtensionApiAlias ,
2026-03-19 03:12:16 +00:00
resolvePluginSdkAliasCandidateOrder ,
resolvePluginSdkAliasFile ,
2026-03-21 20:17:47 +00:00
resolvePluginRuntimeModulePath ,
2026-03-19 03:12:16 +00:00
resolvePluginSdkScopedAliasMap ,
shouldPreferNativeJiti ,
} from "./sdk-alias.js" ;
2026-03-30 22:36:48 -06:00
import { hasKind , kindsEqual } from "./slots.js" ;
2026-02-18 01:34:35 +00:00
import type {
OpenClawPluginDefinition ,
OpenClawPluginModule ,
PluginDiagnostic ,
2026-03-15 16:08:30 -07:00
PluginBundleFormat ,
PluginFormat ,
2026-02-18 01:34:35 +00:00
PluginLogger ,
} from "./types.js" ;
2026-01-11 12:11:12 +00:00
export type PluginLoadResult = PluginRegistry ;
export type PluginLoadOptions = {
2026-01-30 03:15:10 +01:00
config? : OpenClawConfig ;
2026-04-02 20:57:14 +09:00
activationSourceConfig? : OpenClawConfig ;
autoEnabledReasons? : Readonly < Record < string , string [ ] > > ;
2026-01-11 12:11:12 +00:00
workspaceDir? : string ;
2026-03-12 15:31:31 +00:00
// Allows callers to resolve plugin roots and load paths against an explicit env
// instead of the process-global environment.
env? : NodeJS.ProcessEnv ;
2026-01-11 12:11:12 +00:00
logger? : PluginLogger ;
coreGatewayHandlers? : Record < string , GatewayRequestHandler > ;
2026-03-06 05:31:59 -08:00
runtimeOptions? : CreatePluginRuntimeOptions ;
2026-03-27 13:38:40 +00:00
pluginSdkResolution? : PluginSdkResolutionPreference ;
2026-01-11 12:11:12 +00:00
cache? : boolean ;
2026-01-19 03:38:51 +00:00
mode ? : "full" | "validate" ;
2026-03-16 07:52:08 +08:00
onlyPluginIds? : string [ ] ;
2026-03-15 18:46:22 -07:00
includeSetupOnlyChannelPlugins? : boolean ;
2026-03-16 13:55:53 +00:00
/**
* Prefer `setupEntry` for configured channel plugins that explicitly opt in
* via package metadata because their setup entry covers the pre-listen startup surface.
*/
2026-03-16 13:30:24 +00:00
preferSetupRuntimeForChannelPlugins? : boolean ;
2026-03-16 07:52:08 +08:00
activate? : boolean ;
2026-04-02 22:50:17 +09:00
loadModules? : boolean ;
2026-03-19 16:04:19 -07:00
throwOnLoadError? : boolean ;
2026-01-11 12:11:12 +00:00
} ;
2026-04-04 02:35:23 +01:00
const CLI_METADATA_ENTRY_BASENAMES = [
"cli-metadata.ts" ,
"cli-metadata.js" ,
"cli-metadata.mjs" ,
"cli-metadata.cjs" ,
] as const ;
2026-03-19 16:04:19 -07:00
export class PluginLoadFailureError extends Error {
readonly pluginIds : string [ ] ;
readonly registry : PluginRegistry ;
constructor ( registry : PluginRegistry ) {
const failedPlugins = registry . plugins . filter ( ( entry ) = > entry . status === "error" ) ;
const summary = failedPlugins
. map ( ( entry ) = > ` ${ entry . id } : ${ entry . error ? ? "unknown plugin load error" } ` )
. join ( "; " ) ;
super ( ` plugin load failed: ${ summary } ` ) ;
this . name = "PluginLoadFailureError" ;
this . pluginIds = failedPlugins . map ( ( entry ) = > entry . id ) ;
this . registry = registry ;
}
}
2026-03-21 20:20:45 +02:00
type CachedPluginState = {
registry : PluginRegistry ;
2026-03-27 02:01:07 +00:00
memoryEmbeddingProviders : ReturnType < typeof listRegisteredMemoryEmbeddingProviders > ;
2026-03-26 21:30:39 +00:00
memoryFlushPlanResolver : ReturnType < typeof getMemoryFlushPlanResolver > ;
2026-03-21 20:20:45 +02:00
memoryPromptBuilder : ReturnType < typeof getMemoryPromptSectionBuilder > ;
2026-03-26 22:29:38 +00:00
memoryRuntime : ReturnType < typeof getMemoryRuntime > ;
2026-03-21 20:20:45 +02:00
} ;
2026-03-15 17:29:10 -07:00
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128 ;
2026-03-21 23:30:51 +00:00
let pluginRegistryCacheEntryCap = MAX_PLUGIN_REGISTRY_CACHE_ENTRIES ;
2026-03-21 20:20:45 +02:00
const registryCache = new Map < string , CachedPluginState > ( ) ;
2026-03-13 11:24:40 -07:00
const openAllowlistWarningCache = new Set < string > ( ) ;
2026-03-16 11:39:26 +00:00
const LAZY_RUNTIME_REFLECTION_KEYS = [
"version" ,
"config" ,
2026-03-16 15:51:08 -05:00
"agent" ,
2026-03-16 11:39:26 +00:00
"subagent" ,
"system" ,
"media" ,
"tts" ,
"stt" ,
"channel" ,
"events" ,
"logging" ,
"state" ,
"modelAuth" ,
] as const satisfies readonly ( keyof PluginRuntime ) [ ] ;
2026-01-11 12:11:12 +00:00
2026-03-12 15:31:31 +00:00
export function clearPluginLoaderCache ( ) : void {
registryCache . clear ( ) ;
2026-03-13 11:24:40 -07:00
openAllowlistWarningCache . clear ( ) ;
2026-03-27 00:40:45 +00:00
clearMemoryEmbeddingProviders ( ) ;
2026-03-26 22:14:33 +00:00
clearMemoryPluginState ( ) ;
2026-03-12 15:31:31 +00:00
}
2026-01-11 12:11:12 +00:00
const defaultLogger = ( ) = > createSubsystemLogger ( "plugins" ) ;
2026-03-29 18:49:57 -04:00
function createPluginJitiLoader ( options : Pick < PluginLoadOptions , "pluginSdkResolution" > ) {
const jitiLoaders = new Map < string , ReturnType < typeof createJiti > > ( ) ;
return ( modulePath : string ) = > {
const tryNative = shouldPreferNativeJiti ( modulePath ) ;
const aliasMap = buildPluginLoaderAliasMap (
modulePath ,
process . argv [ 1 ] ,
import . meta . url ,
options . pluginSdkResolution ,
) ;
const cacheKey = JSON . stringify ( {
tryNative ,
aliasMap : Object.entries ( aliasMap ) . toSorted ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) ) ,
} ) ;
const cached = jitiLoaders . get ( cacheKey ) ;
if ( cached ) {
return cached ;
}
const loader = createJiti ( import . meta . url , {
. . . buildPluginLoaderJitiOptions ( aliasMap ) ,
// Source .ts runtime shims import sibling ".js" specifiers that only exist
// after build. Disable native loading for source entries so Jiti rewrites
// those imports against the source graph, while keeping native dist/*.js
// loading for the canonical built module graph.
tryNative ,
} ) ;
jitiLoaders . set ( cacheKey , loader ) ;
return loader ;
} ;
}
2026-02-26 11:00:09 +01:00
export const __testing = {
2026-03-16 20:58:58 -04:00
buildPluginLoaderJitiOptions ,
2026-03-19 15:06:35 -07:00
buildPluginLoaderAliasMap ,
2026-03-08 00:28:36 +00:00
listPluginSdkAliasCandidates ,
2026-03-08 17:08:33 +00:00
listPluginSdkExportedSubpaths ,
2026-03-20 12:41:19 -05:00
resolveExtensionApiAlias ,
2026-03-19 03:29:37 +00:00
resolvePluginSdkScopedAliasMap ,
2026-03-08 00:28:36 +00:00
resolvePluginSdkAliasCandidateOrder ,
2026-02-26 11:00:09 +01:00
resolvePluginSdkAliasFile ,
2026-03-16 05:12:19 +00:00
resolvePluginRuntimeModulePath ,
2026-03-18 16:24:37 +00:00
shouldPreferNativeJiti ,
2026-03-28 00:23:17 -04:00
getCompatibleActivePluginRegistry ,
resolvePluginLoadCacheContext ,
2026-03-21 23:30:51 +00:00
get maxPluginRegistryCacheEntries() {
return pluginRegistryCacheEntryCap ;
} ,
setMaxPluginRegistryCacheEntriesForTest ( value? : number ) {
pluginRegistryCacheEntryCap =
typeof value === "number" && Number . isFinite ( value ) && value > 0
? Math . max ( 1 , Math . floor ( value ) )
: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES ;
} ,
2026-02-26 11:00:09 +01:00
} ;
2026-03-21 20:20:45 +02:00
function getCachedPluginRegistry ( cacheKey : string ) : CachedPluginState | undefined {
2026-03-12 15:31:31 +00:00
const cached = registryCache . get ( cacheKey ) ;
if ( ! cached ) {
return undefined ;
}
// Refresh insertion order so frequently reused registries survive eviction.
registryCache . delete ( cacheKey ) ;
registryCache . set ( cacheKey , cached ) ;
return cached ;
}
2026-03-21 20:20:45 +02:00
function setCachedPluginRegistry ( cacheKey : string , state : CachedPluginState ) : void {
2026-03-12 15:31:31 +00:00
if ( registryCache . has ( cacheKey ) ) {
registryCache . delete ( cacheKey ) ;
}
2026-03-21 20:20:45 +02:00
registryCache . set ( cacheKey , state ) ;
2026-03-21 23:30:51 +00:00
while ( registryCache . size > pluginRegistryCacheEntryCap ) {
2026-03-12 15:31:31 +00:00
const oldestKey = registryCache . keys ( ) . next ( ) . value ;
if ( ! oldestKey ) {
break ;
}
registryCache . delete ( oldestKey ) ;
}
}
2026-01-11 12:11:12 +00:00
function buildCacheKey ( params : {
workspaceDir? : string ;
plugins : NormalizedPluginsConfig ;
2026-04-02 20:57:14 +09:00
activationMetadataKey? : string ;
2026-03-12 15:31:31 +00:00
installs? : Record < string , PluginInstallRecord > ;
env : NodeJS.ProcessEnv ;
2026-03-16 07:52:08 +08:00
onlyPluginIds? : string [ ] ;
2026-03-15 18:46:22 -07:00
includeSetupOnlyChannelPlugins? : boolean ;
2026-03-16 13:30:24 +00:00
preferSetupRuntimeForChannelPlugins? : boolean ;
2026-04-02 22:50:17 +09:00
loadModules? : boolean ;
2026-03-16 14:27:54 -07:00
runtimeSubagentMode ? : "default" | "explicit" | "gateway-bindable" ;
2026-03-27 13:38:40 +00:00
pluginSdkResolution? : PluginSdkResolutionPreference ;
2026-03-27 23:21:51 -05:00
coreGatewayMethodNames? : string [ ] ;
2026-01-11 12:11:12 +00:00
} ) : string {
2026-03-12 15:31:31 +00:00
const { roots , loadPaths } = resolvePluginCacheInputs ( {
workspaceDir : params.workspaceDir ,
loadPaths : params.plugins.loadPaths ,
env : params.env ,
} ) ;
const installs = Object . fromEntries (
Object . entries ( params . installs ? ? { } ) . map ( ( [ pluginId , install ] ) = > [
pluginId ,
{
. . . install ,
installPath :
typeof install . installPath === "string"
? resolveUserPath ( install . installPath , params . env )
: install . installPath ,
sourcePath :
typeof install . sourcePath === "string"
? resolveUserPath ( install . sourcePath , params . env )
: install . sourcePath ,
} ,
] ) ,
) ;
2026-03-16 07:52:08 +08:00
const scopeKey = JSON . stringify ( params . onlyPluginIds ? ? [ ] ) ;
2026-03-15 18:46:22 -07:00
const setupOnlyKey = params . includeSetupOnlyChannelPlugins === true ? "setup-only" : "runtime" ;
2026-03-16 13:30:24 +00:00
const startupChannelMode =
params . preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full" ;
2026-04-02 22:50:17 +09:00
const moduleLoadMode = params . loadModules === false ? "manifest-only" : "load-modules" ;
2026-03-27 23:21:51 -05:00
const gatewayMethodsKey = JSON . stringify ( params . coreGatewayMethodNames ? ? [ ] ) ;
2026-03-12 15:31:31 +00:00
return ` ${ roots . workspace ? ? "" } :: ${ roots . global ? ? "" } :: ${ roots . stock ? ? "" } :: ${ JSON . stringify ( {
. . . params . plugins ,
installs ,
loadPaths ,
2026-04-02 20:57:14 +09:00
activationMetadataKey : params.activationMetadataKey ? ? "" ,
2026-04-02 22:50:17 +09:00
} )}:: ${ scopeKey } :: ${ setupOnlyKey } :: ${ startupChannelMode } :: ${ moduleLoadMode } :: ${ params . runtimeSubagentMode ? ? "default" } :: ${ params . pluginSdkResolution ? ? "auto" } :: ${ gatewayMethodsKey } ` ;
2026-03-16 07:52:08 +08:00
}
function normalizeScopedPluginIds ( ids? : string [ ] ) : string [ ] | undefined {
if ( ! ids ) {
return undefined ;
}
const normalized = Array . from ( new Set ( ids . map ( ( id ) = > id . trim ( ) ) . filter ( Boolean ) ) ) . toSorted ( ) ;
return normalized . length > 0 ? normalized : undefined ;
2026-01-11 12:11:12 +00:00
}
2026-04-02 11:18:49 +09:00
function matchesScopedPluginRequest ( params : {
onlyPluginIdSet : ReadonlySet < string > | null ;
pluginId : string ;
} ) : boolean {
const scopedIds = params . onlyPluginIdSet ;
if ( ! scopedIds ) {
return true ;
}
return scopedIds . has ( params . pluginId ) ;
}
2026-03-28 00:06:52 -04:00
function resolveRuntimeSubagentMode (
runtimeOptions : PluginLoadOptions [ "runtimeOptions" ] ,
) : "default" | "explicit" | "gateway-bindable" {
if ( runtimeOptions ? . allowGatewaySubagentBinding === true ) {
return "gateway-bindable" ;
}
if ( runtimeOptions ? . subagent ) {
return "explicit" ;
}
return "default" ;
}
2026-04-02 20:57:14 +09:00
function buildActivationMetadataHash ( params : {
2026-04-03 23:36:52 +09:00
activationSource : PluginActivationConfigSource ;
2026-04-02 20:57:14 +09:00
autoEnabledReasons : Readonly < Record < string , string [ ] > > ;
} ) : string {
const enabledSourceChannels = Object . entries (
2026-04-03 23:36:52 +09:00
( params . activationSource . rootConfig ? . channels as Record < string , unknown > ) ? ? { } ,
2026-04-02 20:57:14 +09:00
)
. filter ( ( [ , value ] ) = > {
if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) {
return false ;
}
return ( value as { enabled? : unknown } ) . enabled === true ;
} )
. map ( ( [ channelId ] ) = > channelId )
. toSorted ( ( left , right ) = > left . localeCompare ( right ) ) ;
2026-04-03 23:36:52 +09:00
const pluginEntryStates = Object . entries ( params . activationSource . plugins . entries )
2026-04-02 20:57:14 +09:00
. map ( ( [ pluginId , entry ] ) = > [ pluginId , entry ? . enabled ? ? null ] as const )
. toSorted ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) ) ;
const autoEnableReasonEntries = Object . entries ( params . autoEnabledReasons )
. map ( ( [ pluginId , reasons ] ) = > [ pluginId , [ . . . reasons ] ] as const )
. toSorted ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) ) ;
return createHash ( "sha256" )
. update (
JSON . stringify ( {
2026-04-03 23:36:52 +09:00
enabled : params.activationSource.plugins.enabled ,
allow : params.activationSource.plugins.allow ,
deny : params.activationSource.plugins.deny ,
memorySlot : params.activationSource.plugins.slots.memory ,
2026-04-02 20:57:14 +09:00
entries : pluginEntryStates ,
enabledChannels : enabledSourceChannels ,
autoEnabledReasons : autoEnableReasonEntries ,
} ) ,
)
. digest ( "hex" ) ;
}
2026-03-28 00:06:52 -04:00
function hasExplicitCompatibilityInputs ( options : PluginLoadOptions ) : boolean {
return Boolean (
options . config !== undefined ||
2026-04-02 20:57:14 +09:00
options . activationSourceConfig !== undefined ||
options . autoEnabledReasons !== undefined ||
2026-03-28 00:06:52 -04:00
options . workspaceDir !== undefined ||
options . env !== undefined ||
options . onlyPluginIds ? . length ||
options . runtimeOptions !== undefined ||
options . pluginSdkResolution !== undefined ||
2026-03-27 23:21:51 -05:00
options . coreGatewayHandlers !== undefined ||
2026-03-28 00:06:52 -04:00
options . includeSetupOnlyChannelPlugins === true ||
2026-04-02 22:50:17 +09:00
options . preferSetupRuntimeForChannelPlugins === true ||
options . loadModules === false ,
2026-03-28 00:06:52 -04:00
) ;
}
2026-03-28 00:23:17 -04:00
function resolvePluginLoadCacheContext ( options : PluginLoadOptions = { } ) {
2026-03-28 00:06:52 -04:00
const env = options . env ? ? process . env ;
const cfg = applyTestPluginDefaults ( options . config ? ? { } , env ) ;
2026-04-02 20:57:14 +09:00
const activationSourceConfig = options . activationSourceConfig ? ? options . config ? ? { } ;
2026-03-28 00:06:52 -04:00
const normalized = normalizePluginsConfig ( cfg . plugins ) ;
2026-04-03 23:36:52 +09:00
const activationSource = createPluginActivationSource ( {
config : activationSourceConfig ,
} ) ;
2026-03-28 00:06:52 -04:00
const onlyPluginIds = normalizeScopedPluginIds ( options . onlyPluginIds ) ;
const includeSetupOnlyChannelPlugins = options . includeSetupOnlyChannelPlugins === true ;
const preferSetupRuntimeForChannelPlugins = options . preferSetupRuntimeForChannelPlugins === true ;
2026-03-27 23:21:51 -05:00
const coreGatewayMethodNames = Object . keys ( options . coreGatewayHandlers ? ? { } ) . toSorted ( ) ;
2026-03-28 00:06:52 -04:00
const cacheKey = buildCacheKey ( {
workspaceDir : options.workspaceDir ,
plugins : normalized ,
2026-04-02 20:57:14 +09:00
activationMetadataKey : buildActivationMetadataHash ( {
2026-04-03 23:36:52 +09:00
activationSource ,
2026-04-02 20:57:14 +09:00
autoEnabledReasons : options.autoEnabledReasons ? ? { } ,
} ) ,
2026-03-28 00:06:52 -04:00
installs : cfg.plugins?.installs ,
env ,
onlyPluginIds ,
includeSetupOnlyChannelPlugins ,
preferSetupRuntimeForChannelPlugins ,
2026-04-02 22:50:17 +09:00
loadModules : options.loadModules ,
2026-03-28 00:06:52 -04:00
runtimeSubagentMode : resolveRuntimeSubagentMode ( options . runtimeOptions ) ,
pluginSdkResolution : options.pluginSdkResolution ,
2026-03-27 23:21:51 -05:00
coreGatewayMethodNames ,
2026-03-28 00:06:52 -04:00
} ) ;
return {
env ,
cfg ,
normalized ,
2026-04-02 20:57:14 +09:00
activationSourceConfig ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-04-02 20:57:14 +09:00
autoEnabledReasons : options.autoEnabledReasons ? ? { } ,
2026-03-28 00:06:52 -04:00
onlyPluginIds ,
includeSetupOnlyChannelPlugins ,
preferSetupRuntimeForChannelPlugins ,
shouldActivate : options.activate !== false ,
2026-04-02 22:50:17 +09:00
shouldLoadModules : options.loadModules !== false ,
2026-03-29 09:53:30 +05:30
runtimeSubagentMode : resolveRuntimeSubagentMode ( options . runtimeOptions ) ,
2026-03-28 00:06:52 -04:00
cacheKey ,
} ;
}
2026-03-28 00:23:17 -04:00
function getCompatibleActivePluginRegistry (
2026-03-28 00:06:52 -04:00
options : PluginLoadOptions = { } ,
) : PluginRegistry | undefined {
const activeRegistry = getActivePluginRegistry ( ) ? ? undefined ;
if ( ! activeRegistry ) {
return undefined ;
}
if ( ! hasExplicitCompatibilityInputs ( options ) ) {
return activeRegistry ;
}
const activeCacheKey = getActivePluginRegistryKey ( ) ;
if ( ! activeCacheKey ) {
return undefined ;
}
return resolvePluginLoadCacheContext ( options ) . cacheKey === activeCacheKey
? activeRegistry
: undefined ;
}
2026-03-28 00:18:50 -04:00
export function resolveRuntimePluginRegistry (
options? : PluginLoadOptions ,
) : PluginRegistry | undefined {
if ( ! options || ! hasExplicitCompatibilityInputs ( options ) ) {
return getCompatibleActivePluginRegistry ( ) ;
}
return getCompatibleActivePluginRegistry ( options ) ? ? loadOpenClawPlugins ( options ) ;
}
2026-04-03 04:07:43 +09:00
export function resolveCompatibleRuntimePluginRegistry (
options? : PluginLoadOptions ,
) : PluginRegistry | undefined {
// Check whether the active runtime registry is already compatible with these
// load options. Unlike resolveRuntimePluginRegistry, this never triggers a
// fresh plugin load on cache miss.
return getCompatibleActivePluginRegistry ( options ) ;
}
2026-01-11 12:11:12 +00:00
function validatePluginConfig ( params : {
2026-01-19 21:13:51 -06:00
schema? : Record < string , unknown > ;
cacheKey? : string ;
value? : unknown ;
2026-01-11 12:11:12 +00:00
} ) : { ok : boolean ; value? : Record < string , unknown > ; errors? : string [ ] } {
const schema = params . schema ;
2026-01-19 21:13:51 -06:00
if ( ! schema ) {
return { ok : true , value : params.value as Record < string , unknown > | undefined } ;
2026-01-11 12:11:12 +00:00
}
2026-01-19 21:13:51 -06:00
const cacheKey = params . cacheKey ? ? JSON . stringify ( schema ) ;
const result = validateJsonSchemaValue ( {
schema ,
cacheKey ,
value : params.value ? ? { } ,
2026-03-27 01:52:30 +00:00
applyDefaults : true ,
2026-01-19 21:13:51 -06:00
} ) ;
if ( result . ok ) {
2026-03-27 01:52:30 +00:00
return { ok : true , value : result.value as Record < string , unknown > | undefined } ;
2026-01-11 12:11:12 +00:00
}
2026-03-02 20:05:12 -05:00
return { ok : false , errors : result.errors.map ( ( error ) = > error . text ) } ;
2026-01-11 12:11:12 +00:00
}
function resolvePluginModuleExport ( moduleExport : unknown ) : {
2026-01-30 03:15:10 +01:00
definition? : OpenClawPluginDefinition ;
register? : OpenClawPluginDefinition [ "register" ] ;
2026-01-11 12:11:12 +00:00
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in ( moduleExport as Record < string , unknown > )
? ( moduleExport as { default : unknown } ) . default
: moduleExport ;
if ( typeof resolved === "function" ) {
return {
2026-01-30 03:15:10 +01:00
register : resolved as OpenClawPluginDefinition [ "register" ] ,
2026-01-11 12:11:12 +00:00
} ;
}
if ( resolved && typeof resolved === "object" ) {
2026-01-30 03:15:10 +01:00
const def = resolved as OpenClawPluginDefinition ;
2026-01-11 12:11:12 +00:00
const register = def . register ? ? def . activate ;
return { definition : def , register } ;
}
return { } ;
}
2026-03-15 18:46:22 -07:00
function resolveSetupChannelRegistration ( moduleExport : unknown ) : {
plugin? : ChannelPlugin ;
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in ( moduleExport as Record < string , unknown > )
? ( moduleExport as { default : unknown } ) . default
: moduleExport ;
if ( ! resolved || typeof resolved !== "object" ) {
return { } ;
}
const setup = resolved as {
plugin? : unknown ;
} ;
if ( ! setup . plugin || typeof setup . plugin !== "object" ) {
return { } ;
}
return {
plugin : setup.plugin as ChannelPlugin ,
} ;
}
2026-03-15 19:27:45 -07:00
function shouldLoadChannelPluginInSetupRuntime ( params : {
manifestChannels : string [ ] ;
setupSource? : string ;
2026-03-16 13:55:53 +00:00
startupDeferConfiguredChannelFullLoadUntilAfterListen? : boolean ;
2026-03-15 19:27:45 -07:00
cfg : OpenClawConfig ;
env : NodeJS.ProcessEnv ;
2026-03-16 13:30:24 +00:00
preferSetupRuntimeForChannelPlugins? : boolean ;
2026-03-15 19:27:45 -07:00
} ) : boolean {
if ( ! params . setupSource || params . manifestChannels . length === 0 ) {
return false ;
}
2026-03-16 13:55:53 +00:00
if (
params . preferSetupRuntimeForChannelPlugins &&
params . startupDeferConfiguredChannelFullLoadUntilAfterListen === true
) {
2026-03-16 13:30:24 +00:00
return true ;
}
2026-03-15 19:27:45 -07:00
return ! params . manifestChannels . some ( ( channelId ) = >
isChannelConfigured ( params . cfg , channelId , params . env ) ,
) ;
}
2026-01-11 12:11:12 +00:00
function createPluginRecord ( params : {
id : string ;
name? : string ;
description? : string ;
version? : string ;
2026-03-15 16:08:30 -07:00
format? : PluginFormat ;
bundleFormat? : PluginBundleFormat ;
bundleCapabilities? : string [ ] ;
2026-01-11 12:11:12 +00:00
source : string ;
2026-03-15 19:06:11 -04:00
rootDir? : string ;
2026-01-11 12:11:12 +00:00
origin : PluginRecord [ "origin" ] ;
workspaceDir? : string ;
enabled : boolean ;
2026-04-02 20:57:14 +09:00
activationState? : PluginActivationState ;
2026-01-11 12:11:12 +00:00
configSchema : boolean ;
2026-04-05 14:43:29 +01:00
contracts? : PluginManifestContracts ;
2026-01-11 12:11:12 +00:00
} ) : PluginRecord {
return {
id : params.id ,
name : params.name ? ? params . id ,
description : params.description ,
version : params.version ,
2026-03-15 16:08:30 -07:00
format : params.format ? ? "openclaw" ,
bundleFormat : params.bundleFormat ,
bundleCapabilities : params.bundleCapabilities ,
2026-01-11 12:11:12 +00:00
source : params.source ,
2026-03-15 19:06:11 -04:00
rootDir : params.rootDir ,
2026-01-11 12:11:12 +00:00
origin : params.origin ,
workspaceDir : params.workspaceDir ,
enabled : params.enabled ,
2026-04-02 20:57:14 +09:00
explicitlyEnabled : params.activationState?.explicitlyEnabled ,
activated : params.activationState?.activated ,
activationSource : params.activationState?.source ,
activationReason : params.activationState?.reason ,
2026-01-11 12:11:12 +00:00
status : params.enabled ? "loaded" : "disabled" ,
toolNames : [ ] ,
2026-01-18 05:56:59 +00:00
hookNames : [ ] ,
2026-01-15 02:42:41 +00:00
channelIds : [ ] ,
2026-01-16 03:15:07 +00:00
providerIds : [ ] ,
2026-03-16 18:49:55 -07:00
speechProviderIds : [ ] ,
2026-04-04 12:04:37 +09:00
realtimeTranscriptionProviderIds : [ ] ,
realtimeVoiceProviderIds : [ ] ,
2026-03-16 20:42:00 -07:00
mediaUnderstandingProviderIds : [ ] ,
2026-03-16 22:56:14 -07:00
imageGenerationProviderIds : [ ] ,
2026-04-04 17:43:15 +01:00
videoGenerationProviderIds : [ ] ,
2026-04-06 01:43:08 +01:00
musicGenerationProviderIds : [ ] ,
2026-04-02 20:25:19 +09:00
webFetchProviderIds : [ ] ,
2026-03-16 00:39:27 +00:00
webSearchProviderIds : [ ] ,
2026-04-05 14:43:29 +01:00
memoryEmbeddingProviderIds : [ ] ,
2026-01-11 12:11:12 +00:00
gatewayMethods : [ ] ,
cliCommands : [ ] ,
services : [ ] ,
2026-01-23 03:17:10 +00:00
commands : [ ] ,
2026-03-02 16:22:31 +00:00
httpRoutes : 0 ,
2026-01-18 05:40:58 +00:00
hookCount : 0 ,
2026-01-11 12:11:12 +00:00
configSchema : params.configSchema ,
2026-01-12 01:16:39 +00:00
configUiHints : undefined ,
2026-01-16 14:13:30 -06:00
configJsonSchema : undefined ,
2026-04-05 14:43:29 +01:00
contracts : params.contracts ,
2026-01-11 12:11:12 +00:00
} ;
}
2026-04-02 20:57:14 +09:00
function markPluginActivationDisabled ( record : PluginRecord , reason? : string ) : void {
record . activated = false ;
record . activationSource = "disabled" ;
record . activationReason = reason ;
}
function formatAutoEnabledActivationReason (
reasons : readonly string [ ] | undefined ,
) : string | undefined {
if ( ! reasons || reasons . length === 0 ) {
return undefined ;
}
return reasons . join ( "; " ) ;
}
2026-02-22 21:18:53 +00:00
function recordPluginError ( params : {
logger : PluginLogger ;
registry : PluginRegistry ;
record : PluginRecord ;
seenIds : Map < string , PluginRecord [ "origin" ] > ;
pluginId : string ;
origin : PluginRecord [ "origin" ] ;
2026-04-03 23:21:48 +09:00
phase : PluginRecord [ "failurePhase" ] ;
2026-02-22 21:18:53 +00:00
error : unknown ;
logPrefix : string ;
diagnosticMessagePrefix : string ;
} ) {
2026-03-18 13:40:28 -07:00
const errorText =
process . env . OPENCLAW_PLUGIN_LOADER_DEBUG_STACKS === "1" &&
params . error instanceof Error &&
typeof params . error . stack === "string"
? params.error.stack
: String ( params . error ) ;
2026-03-05 23:23:24 -05:00
const deprecatedApiHint =
errorText . includes ( "api.registerHttpHandler" ) && errorText . includes ( "is not a function" )
? "deprecated api.registerHttpHandler(...) was removed; use api.registerHttpRoute(...) for plugin-owned routes or registerPluginHttpRoute(...) for dynamic lifecycle routes"
: null ;
const displayError = deprecatedApiHint ? ` ${ deprecatedApiHint } ( ${ errorText } ) ` : errorText ;
params . logger . error ( ` ${ params . logPrefix } ${ displayError } ` ) ;
2026-02-22 21:18:53 +00:00
params . record . status = "error" ;
2026-03-05 23:23:24 -05:00
params . record . error = displayError ;
2026-04-03 23:21:48 +09:00
params . record . failedAt = new Date ( ) ;
params . record . failurePhase = params . phase ;
2026-02-22 21:18:53 +00:00
params . registry . plugins . push ( params . record ) ;
params . seenIds . set ( params . pluginId , params . origin ) ;
params . registry . diagnostics . push ( {
level : "error" ,
pluginId : params.record.id ,
source : params.record.source ,
2026-03-05 23:23:24 -05:00
message : ` ${ params . diagnosticMessagePrefix } ${ displayError } ` ,
2026-02-22 21:18:53 +00:00
} ) ;
}
2026-04-03 23:21:48 +09:00
function formatPluginFailureSummary ( failedPlugins : PluginRecord [ ] ) : string {
const grouped = new Map < NonNullable < PluginRecord [ "failurePhase" ] > , string [ ] > ( ) ;
for ( const plugin of failedPlugins ) {
const phase = plugin . failurePhase ? ? "load" ;
const ids = grouped . get ( phase ) ;
if ( ids ) {
ids . push ( plugin . id ) ;
continue ;
}
grouped . set ( phase , [ plugin . id ] ) ;
}
return [ . . . grouped . entries ( ) ] . map ( ( [ phase , ids ] ) = > ` ${ phase } : ${ ids . join ( ", " ) } ` ) . join ( "; " ) ;
}
2026-01-14 14:31:43 +00:00
function pushDiagnostics ( diagnostics : PluginDiagnostic [ ] , append : PluginDiagnostic [ ] ) {
2026-01-11 12:11:12 +00:00
diagnostics . push ( . . . append ) ;
}
2026-03-19 16:04:19 -07:00
function maybeThrowOnPluginLoadError (
registry : PluginRegistry ,
throwOnLoadError : boolean | undefined ,
) : void {
if ( ! throwOnLoadError ) {
return ;
}
if ( ! registry . plugins . some ( ( entry ) = > entry . status === "error" ) ) {
return ;
}
throw new PluginLoadFailureError ( registry ) ;
}
2026-02-19 15:24:02 +01:00
type PathMatcher = {
exact : Set < string > ;
dirs : string [ ] ;
} ;
type InstallTrackingRule = {
trackedWithoutPaths : boolean ;
matcher : PathMatcher ;
} ;
type PluginProvenanceIndex = {
loadPathMatcher : PathMatcher ;
installRules : Map < string , InstallTrackingRule > ;
} ;
function createPathMatcher ( ) : PathMatcher {
return { exact : new Set < string > ( ) , dirs : [ ] } ;
}
2026-03-12 15:31:31 +00:00
function addPathToMatcher (
matcher : PathMatcher ,
rawPath : string ,
env : NodeJS.ProcessEnv = process . env ,
) : void {
2026-02-19 15:24:02 +01:00
const trimmed = rawPath . trim ( ) ;
if ( ! trimmed ) {
return ;
2026-02-19 15:13:34 +01:00
}
2026-03-12 15:31:31 +00:00
const resolved = resolveUserPath ( trimmed , env ) ;
2026-02-19 15:24:02 +01:00
if ( ! resolved ) {
return ;
}
if ( matcher . exact . has ( resolved ) || matcher . dirs . includes ( resolved ) ) {
return ;
}
const stat = safeStatSync ( resolved ) ;
if ( stat ? . isDirectory ( ) ) {
matcher . dirs . push ( resolved ) ;
return ;
}
matcher . exact . add ( resolved ) ;
2026-02-19 15:13:34 +01:00
}
2026-02-19 15:24:02 +01:00
function matchesPathMatcher ( matcher : PathMatcher , sourcePath : string ) : boolean {
if ( matcher . exact . has ( sourcePath ) ) {
2026-02-19 15:13:34 +01:00
return true ;
}
2026-02-19 15:24:02 +01:00
return matcher . dirs . some ( ( dirPath ) = > isPathInside ( dirPath , sourcePath ) ) ;
}
function buildProvenanceIndex ( params : {
config : OpenClawConfig ;
normalizedLoadPaths : string [ ] ;
2026-03-12 15:31:31 +00:00
env : NodeJS.ProcessEnv ;
2026-02-19 15:24:02 +01:00
} ) : PluginProvenanceIndex {
const loadPathMatcher = createPathMatcher ( ) ;
for ( const loadPath of params . normalizedLoadPaths ) {
2026-03-12 15:31:31 +00:00
addPathToMatcher ( loadPathMatcher , loadPath , params . env ) ;
2026-02-19 15:24:02 +01:00
}
const installRules = new Map < string , InstallTrackingRule > ( ) ;
const installs = params . config . plugins ? . installs ? ? { } ;
for ( const [ pluginId , install ] of Object . entries ( installs ) ) {
const rule : InstallTrackingRule = {
trackedWithoutPaths : false ,
matcher : createPathMatcher ( ) ,
} ;
const trackedPaths = [ install . installPath , install . sourcePath ]
. map ( ( entry ) = > ( typeof entry === "string" ? entry . trim ( ) : "" ) )
. filter ( Boolean ) ;
if ( trackedPaths . length === 0 ) {
rule . trackedWithoutPaths = true ;
} else {
for ( const trackedPath of trackedPaths ) {
2026-03-12 15:31:31 +00:00
addPathToMatcher ( rule . matcher , trackedPath , params . env ) ;
2026-02-19 15:24:02 +01:00
}
2026-02-19 15:13:34 +01:00
}
2026-02-19 15:24:02 +01:00
installRules . set ( pluginId , rule ) ;
2026-02-19 15:13:34 +01:00
}
2026-02-19 15:24:02 +01:00
return { loadPathMatcher , installRules } ;
2026-02-19 15:13:34 +01:00
}
2026-02-19 15:24:02 +01:00
function isTrackedByProvenance ( params : {
2026-02-19 15:13:34 +01:00
pluginId : string ;
source : string ;
2026-02-19 15:24:02 +01:00
index : PluginProvenanceIndex ;
2026-03-12 15:31:31 +00:00
env : NodeJS.ProcessEnv ;
2026-02-19 15:13:34 +01:00
} ) : boolean {
2026-03-12 15:31:31 +00:00
const sourcePath = resolveUserPath ( params . source , params . env ) ;
2026-02-19 15:24:02 +01:00
const installRule = params . index . installRules . get ( params . pluginId ) ;
if ( installRule ) {
if ( installRule . trackedWithoutPaths ) {
return true ;
}
if ( matchesPathMatcher ( installRule . matcher , sourcePath ) ) {
return true ;
}
2026-02-19 15:13:34 +01:00
}
2026-02-19 15:24:02 +01:00
return matchesPathMatcher ( params . index . loadPathMatcher , sourcePath ) ;
2026-02-19 15:13:34 +01:00
}
2026-03-14 21:08:32 -05:00
function matchesExplicitInstallRule ( params : {
pluginId : string ;
source : string ;
index : PluginProvenanceIndex ;
env : NodeJS.ProcessEnv ;
} ) : boolean {
const sourcePath = resolveUserPath ( params . source , params . env ) ;
const installRule = params . index . installRules . get ( params . pluginId ) ;
if ( ! installRule || installRule . trackedWithoutPaths ) {
return false ;
}
return matchesPathMatcher ( installRule . matcher , sourcePath ) ;
}
function resolveCandidateDuplicateRank ( params : {
candidate : ReturnType < typeof discoverOpenClawPlugins > [ "candidates" ] [ number ] ;
manifestByRoot : Map < string , ReturnType < typeof loadPluginManifestRegistry > [ "plugins" ] [ number ] > ;
provenance : PluginProvenanceIndex ;
env : NodeJS.ProcessEnv ;
} ) : number {
const manifestRecord = params . manifestByRoot . get ( params . candidate . rootDir ) ;
const pluginId = manifestRecord ? . id ;
const isExplicitInstall =
params . candidate . origin === "global" &&
pluginId !== undefined &&
matchesExplicitInstallRule ( {
pluginId ,
source : params.candidate.source ,
index : params.provenance ,
env : params.env ,
} ) ;
2026-03-15 09:07:10 -07:00
if ( params . candidate . origin === "config" ) {
return 0 ;
2026-03-14 21:08:32 -05:00
}
2026-03-15 09:07:10 -07:00
if ( params . candidate . origin === "global" && isExplicitInstall ) {
return 1 ;
}
if ( params . candidate . origin === "bundled" ) {
// Bundled plugin ids stay reserved unless the operator configured an override.
return 2 ;
}
if ( params . candidate . origin === "workspace" ) {
return 3 ;
}
return 4 ;
2026-03-14 21:08:32 -05:00
}
function compareDuplicateCandidateOrder ( params : {
left : ReturnType < typeof discoverOpenClawPlugins > [ "candidates" ] [ number ] ;
right : ReturnType < typeof discoverOpenClawPlugins > [ "candidates" ] [ number ] ;
manifestByRoot : Map < string , ReturnType < typeof loadPluginManifestRegistry > [ "plugins" ] [ number ] > ;
provenance : PluginProvenanceIndex ;
env : NodeJS.ProcessEnv ;
} ) : number {
const leftPluginId = params . manifestByRoot . get ( params . left . rootDir ) ? . id ;
const rightPluginId = params . manifestByRoot . get ( params . right . rootDir ) ? . id ;
if ( ! leftPluginId || leftPluginId !== rightPluginId ) {
return 0 ;
}
return (
resolveCandidateDuplicateRank ( {
candidate : params.left ,
manifestByRoot : params.manifestByRoot ,
provenance : params.provenance ,
env : params.env ,
} ) -
resolveCandidateDuplicateRank ( {
candidate : params.right ,
manifestByRoot : params.manifestByRoot ,
provenance : params.provenance ,
env : params.env ,
} )
) ;
}
2026-02-19 15:13:34 +01:00
function warnWhenAllowlistIsOpen ( params : {
2026-04-05 13:08:43 -04:00
emitWarning : boolean ;
2026-02-19 15:13:34 +01:00
logger : PluginLogger ;
pluginsEnabled : boolean ;
allow : string [ ] ;
2026-03-13 11:24:40 -07:00
warningCacheKey : string ;
2026-02-19 15:13:34 +01:00
discoverablePlugins : Array < { id : string ; source : string ; origin : PluginRecord [ "origin" ] } > ;
} ) {
2026-04-05 13:08:43 -04:00
if ( ! params . emitWarning ) {
return ;
}
2026-02-19 15:13:34 +01:00
if ( ! params . pluginsEnabled ) {
return ;
}
if ( params . allow . length > 0 ) {
return ;
}
2026-03-26 11:22:53 +05:30
const autoDiscoverable = params . discoverablePlugins . filter (
( entry ) = > entry . origin === "workspace" || entry . origin === "global" ,
) ;
if ( autoDiscoverable . length === 0 ) {
2026-02-19 15:13:34 +01:00
return ;
}
2026-03-13 11:24:40 -07:00
if ( openAllowlistWarningCache . has ( params . warningCacheKey ) ) {
return ;
}
2026-03-26 11:22:53 +05:30
const preview = autoDiscoverable
2026-02-19 15:13:34 +01:00
. slice ( 0 , 6 )
. map ( ( entry ) = > ` ${ entry . id } ( ${ entry . source } ) ` )
. join ( ", " ) ;
2026-03-26 11:22:53 +05:30
const extra = autoDiscoverable . length > 6 ? ` (+ ${ autoDiscoverable . length - 6 } more) ` : "" ;
2026-03-13 11:24:40 -07:00
openAllowlistWarningCache . add ( params . warningCacheKey ) ;
2026-02-19 15:13:34 +01:00
params . logger . warn (
` [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load: ${ preview } ${ extra } . Set plugins.allow to explicit trusted ids. ` ,
) ;
}
function warnAboutUntrackedLoadedPlugins ( params : {
registry : PluginRegistry ;
2026-02-19 15:24:02 +01:00
provenance : PluginProvenanceIndex ;
2026-04-01 04:10:30 +03:00
allowlist : string [ ] ;
2026-04-05 13:08:43 -04:00
emitWarning : boolean ;
2026-02-19 15:13:34 +01:00
logger : PluginLogger ;
2026-03-12 15:31:31 +00:00
env : NodeJS.ProcessEnv ;
2026-02-19 15:13:34 +01:00
} ) {
2026-04-01 04:10:30 +03:00
const allowSet = new Set ( params . allowlist ) ;
2026-02-19 15:13:34 +01:00
for ( const plugin of params . registry . plugins ) {
if ( plugin . status !== "loaded" || plugin . origin === "bundled" ) {
continue ;
}
2026-04-01 04:10:30 +03:00
if ( allowSet . has ( plugin . id ) ) {
continue ;
}
2026-02-19 15:13:34 +01:00
if (
2026-02-19 15:24:02 +01:00
isTrackedByProvenance ( {
2026-02-19 15:13:34 +01:00
pluginId : plugin.id ,
source : plugin.source ,
2026-02-19 15:24:02 +01:00
index : params.provenance ,
2026-03-12 15:31:31 +00:00
env : params.env ,
2026-02-19 15:13:34 +01:00
} )
) {
continue ;
}
const message =
"loaded without install/load-path provenance; treat as untracked local code and pin trust via plugins.allow or install records" ;
params . registry . diagnostics . push ( {
level : "warn" ,
pluginId : plugin.id ,
source : plugin.source ,
message ,
} ) ;
2026-04-05 13:08:43 -04:00
if ( params . emitWarning ) {
params . logger . warn ( ` [plugins] ${ plugin . id } : ${ message } ( ${ plugin . source } ) ` ) ;
}
2026-02-19 15:13:34 +01:00
}
}
2026-03-29 09:53:30 +05:30
function activatePluginRegistry (
registry : PluginRegistry ,
cacheKey : string ,
runtimeSubagentMode : "default" | "explicit" | "gateway-bindable" ,
2026-04-05 11:03:54 +03:00
workspaceDir? : string ,
2026-03-29 09:53:30 +05:30
) : void {
2026-04-05 11:03:54 +03:00
setActivePluginRegistry ( registry , cacheKey , runtimeSubagentMode , workspaceDir ) ;
2026-03-02 04:04:02 +00:00
initializeGlobalHookRunner ( registry ) ;
}
2026-01-30 03:15:10 +01:00
export function loadOpenClawPlugins ( options : PluginLoadOptions = { } ) : PluginRegistry {
2026-03-16 07:52:08 +08:00
// Snapshot (non-activating) loads must disable the cache to avoid storing a registry
// whose commands were never globally registered.
if ( options . activate === false && options . cache !== false ) {
throw new Error (
"loadOpenClawPlugins: activate:false requires cache:false to prevent command registry divergence" ,
) ;
}
2026-03-28 00:06:52 -04:00
const {
2026-03-12 15:31:31 +00:00
env ,
2026-03-28 00:06:52 -04:00
cfg ,
normalized ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-04-02 20:57:14 +09:00
autoEnabledReasons ,
2026-03-16 07:52:08 +08:00
onlyPluginIds ,
2026-03-15 18:46:22 -07:00
includeSetupOnlyChannelPlugins ,
2026-03-16 13:30:24 +00:00
preferSetupRuntimeForChannelPlugins ,
2026-03-28 00:06:52 -04:00
shouldActivate ,
2026-04-02 22:50:17 +09:00
shouldLoadModules ,
2026-03-28 00:06:52 -04:00
cacheKey ,
2026-03-29 09:53:30 +05:30
runtimeSubagentMode ,
2026-03-28 00:06:52 -04:00
} = resolvePluginLoadCacheContext ( options ) ;
const logger = options . logger ? ? defaultLogger ( ) ;
const validateOnly = options . mode === "validate" ;
const onlyPluginIdSet = onlyPluginIds ? new Set ( onlyPluginIds ) : null ;
2026-01-11 12:11:12 +00:00
const cacheEnabled = options . cache !== false ;
if ( cacheEnabled ) {
2026-03-12 15:31:31 +00:00
const cached = getCachedPluginRegistry ( cacheKey ) ;
2026-01-15 02:42:41 +00:00
if ( cached ) {
2026-03-27 02:01:07 +00:00
restoreRegisteredMemoryEmbeddingProviders ( cached . memoryEmbeddingProviders ) ;
2026-03-26 22:14:33 +00:00
restoreMemoryPluginState ( {
promptBuilder : cached.memoryPromptBuilder ,
flushPlanResolver : cached.memoryFlushPlanResolver ,
2026-03-26 22:29:38 +00:00
runtime : cached.memoryRuntime ,
2026-03-26 22:14:33 +00:00
} ) ;
2026-03-16 07:52:08 +08:00
if ( shouldActivate ) {
2026-04-05 11:03:54 +03:00
activatePluginRegistry (
cached . registry ,
cacheKey ,
runtimeSubagentMode ,
options . workspaceDir ,
) ;
2026-03-16 07:52:08 +08:00
}
2026-03-21 20:20:45 +02:00
return cached . registry ;
2026-01-15 02:42:41 +00:00
}
2026-01-11 12:11:12 +00:00
}
2026-03-21 20:20:45 +02:00
// Clear previously registered plugin state before reloading.
2026-03-16 07:52:08 +08:00
// Skip for non-activating (snapshot) loads to avoid wiping commands from other plugins.
if ( shouldActivate ) {
clearPluginCommands ( ) ;
clearPluginInteractiveHandlers ( ) ;
2026-03-26 22:14:33 +00:00
clearMemoryPluginState ( ) ;
2026-03-16 07:52:08 +08:00
}
2026-01-23 12:43:39 +00:00
2026-03-15 20:02:24 -07:00
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
2026-03-29 18:49:57 -04:00
const getJiti = createPluginJitiLoader ( options ) ;
2026-03-15 20:02:24 -07:00
let createPluginRuntimeFactory : ( ( options? : CreatePluginRuntimeOptions ) = > PluginRuntime ) | null =
null ;
const resolveCreatePluginRuntime = ( ) : ( (
options? : CreatePluginRuntimeOptions ,
) = > PluginRuntime ) = > {
if ( createPluginRuntimeFactory ) {
return createPluginRuntimeFactory ;
}
2026-03-27 13:38:40 +00:00
const runtimeModulePath = resolvePluginRuntimeModulePath ( {
pluginSdkResolution : options.pluginSdkResolution ,
} ) ;
2026-03-15 20:02:24 -07:00
if ( ! runtimeModulePath ) {
throw new Error ( "Unable to resolve plugin runtime module" ) ;
}
2026-03-18 16:24:37 +00:00
const runtimeModule = getJiti ( runtimeModulePath ) ( runtimeModulePath ) as {
2026-03-15 20:02:24 -07:00
createPluginRuntime ? : ( options? : CreatePluginRuntimeOptions ) = > PluginRuntime ;
} ;
if ( typeof runtimeModule . createPluginRuntime !== "function" ) {
throw new Error ( "Plugin runtime module missing createPluginRuntime export" ) ;
}
createPluginRuntimeFactory = runtimeModule . createPluginRuntime ;
return createPluginRuntimeFactory ;
} ;
2026-03-04 02:58:48 +02:00
// Lazily initialize the runtime so startup paths that discover/skip plugins do
2026-03-15 20:02:24 -07:00
// not eagerly load every channel/runtime dependency tree.
2026-03-04 02:58:48 +02:00
let resolvedRuntime : PluginRuntime | null = null ;
const resolveRuntime = ( ) : PluginRuntime = > {
2026-03-15 20:02:24 -07:00
resolvedRuntime ? ? = resolveCreatePluginRuntime ( ) ( options . runtimeOptions ) ;
2026-03-04 02:58:48 +02:00
return resolvedRuntime ;
} ;
2026-03-16 11:39:26 +00:00
const lazyRuntimeReflectionKeySet = new Set < PropertyKey > ( LAZY_RUNTIME_REFLECTION_KEYS ) ;
const resolveLazyRuntimeDescriptor = ( prop : PropertyKey ) : PropertyDescriptor | undefined = > {
if ( ! lazyRuntimeReflectionKeySet . has ( prop ) ) {
return Reflect . getOwnPropertyDescriptor ( resolveRuntime ( ) as object , prop ) ;
}
return {
configurable : true ,
enumerable : true ,
get ( ) {
return Reflect . get ( resolveRuntime ( ) as object , prop ) ;
} ,
set ( value : unknown ) {
Reflect . set ( resolveRuntime ( ) as object , prop , value ) ;
} ,
} ;
} ;
2026-03-04 02:58:48 +02:00
const runtime = new Proxy ( { } as PluginRuntime , {
get ( _target , prop , receiver ) {
return Reflect . get ( resolveRuntime ( ) , prop , receiver ) ;
} ,
set ( _target , prop , value , receiver ) {
return Reflect . set ( resolveRuntime ( ) , prop , value , receiver ) ;
} ,
has ( _target , prop ) {
2026-03-16 11:39:26 +00:00
return lazyRuntimeReflectionKeySet . has ( prop ) || Reflect . has ( resolveRuntime ( ) , prop ) ;
2026-03-04 02:58:48 +02:00
} ,
ownKeys() {
2026-03-16 11:39:26 +00:00
return [ . . . LAZY_RUNTIME_REFLECTION_KEYS ] ;
2026-03-04 02:58:48 +02:00
} ,
getOwnPropertyDescriptor ( _target , prop ) {
2026-03-16 11:39:26 +00:00
return resolveLazyRuntimeDescriptor ( prop ) ;
2026-03-04 02:58:48 +02:00
} ,
defineProperty ( _target , prop , attributes ) {
return Reflect . defineProperty ( resolveRuntime ( ) as object , prop , attributes ) ;
} ,
deleteProperty ( _target , prop ) {
return Reflect . deleteProperty ( resolveRuntime ( ) as object , prop ) ;
} ,
getPrototypeOf() {
return Reflect . getPrototypeOf ( resolveRuntime ( ) as object ) ;
} ,
} ) ;
2026-03-15 20:02:24 -07:00
2026-04-05 23:13:03 +01:00
const {
registry ,
createApi ,
registerReload ,
registerNodeHostCommand ,
registerSecurityAuditCollector ,
} = createPluginRegistry ( {
2026-01-11 12:11:12 +00:00
logger ,
2026-01-18 02:14:07 +00:00
runtime ,
2026-01-14 14:31:43 +00:00
coreGatewayHandlers : options.coreGatewayHandlers as Record < string , GatewayRequestHandler > ,
2026-03-30 21:53:26 +05:30
activateGlobalSideEffects : shouldActivate ,
2026-01-11 12:11:12 +00:00
} ) ;
2026-01-30 03:15:10 +01:00
const discovery = discoverOpenClawPlugins ( {
2026-01-11 12:11:12 +00:00
workspaceDir : options.workspaceDir ,
extraPaths : normalized.loadPaths ,
2026-03-04 01:19:17 -05:00
cache : options.cache ,
2026-03-12 15:31:31 +00:00
env ,
2026-01-11 12:11:12 +00:00
} ) ;
2026-01-19 21:13:51 -06:00
const manifestRegistry = loadPluginManifestRegistry ( {
config : cfg ,
workspaceDir : options.workspaceDir ,
cache : options.cache ,
2026-03-12 15:31:31 +00:00
env ,
2026-01-19 21:13:51 -06:00
candidates : discovery.candidates ,
diagnostics : discovery.diagnostics ,
} ) ;
pushDiagnostics ( registry . diagnostics , manifestRegistry . diagnostics ) ;
2026-02-19 15:13:34 +01:00
warnWhenAllowlistIsOpen ( {
2026-04-05 13:08:43 -04:00
emitWarning : shouldActivate ,
2026-02-19 15:13:34 +01:00
logger ,
pluginsEnabled : normalized.enabled ,
allow : normalized.allow ,
2026-03-13 11:24:40 -07:00
warningCacheKey : cacheKey ,
2026-03-16 07:52:08 +08:00
// Keep warning input scoped as well so partial snapshot loads only mention the
// plugins that were intentionally requested for this registry.
discoverablePlugins : manifestRegistry.plugins
. filter ( ( plugin ) = > ! onlyPluginIdSet || onlyPluginIdSet . has ( plugin . id ) )
. map ( ( plugin ) = > ( {
id : plugin.id ,
source : plugin.source ,
origin : plugin.origin ,
} ) ) ,
2026-02-19 15:13:34 +01:00
} ) ;
2026-02-19 15:24:02 +01:00
const provenance = buildProvenanceIndex ( {
config : cfg ,
normalizedLoadPaths : normalized.loadPaths ,
2026-03-12 15:31:31 +00:00
env ,
2026-02-19 15:24:02 +01:00
} ) ;
2026-01-11 12:11:12 +00:00
2026-01-19 21:13:51 -06:00
const manifestByRoot = new Map (
manifestRegistry . plugins . map ( ( record ) = > [ record . rootDir , record ] ) ,
2026-01-20 08:47:44 +00:00
) ;
2026-03-14 21:08:32 -05:00
const orderedCandidates = [ . . . discovery . candidates ] . toSorted ( ( left , right ) = > {
return compareDuplicateCandidateOrder ( {
left ,
right ,
manifestByRoot ,
provenance ,
env ,
} ) ;
} ) ;
2026-01-20 08:47:44 +00:00
2026-01-17 09:33:56 +00:00
const seenIds = new Map < string , PluginRecord [ "origin" ] > ( ) ;
2026-01-18 02:12:01 +00:00
const memorySlot = normalized . slots . memory ;
let selectedMemoryPluginId : string | null = null ;
let memorySlotMatched = false ;
2026-01-17 09:33:56 +00:00
2026-03-14 21:08:32 -05:00
for ( const candidate of orderedCandidates ) {
2026-01-19 21:13:51 -06:00
const manifestRecord = manifestByRoot . get ( candidate . rootDir ) ;
if ( ! manifestRecord ) {
continue ;
}
const pluginId = manifestRecord . id ;
2026-04-02 11:18:49 +09:00
const matchesRequestedScope = matchesScopedPluginRequest ( {
onlyPluginIdSet ,
pluginId ,
} ) ;
2026-03-16 07:52:08 +08:00
// Filter again at import time as a final guard. The earlier manifest filter keeps
// warnings scoped; this one prevents loading/registering anything outside the scope.
2026-04-02 11:18:49 +09:00
if ( ! matchesRequestedScope ) {
2026-03-16 07:52:08 +08:00
continue ;
}
2026-04-02 20:57:14 +09:00
const activationState = resolveEffectivePluginActivationState ( {
id : pluginId ,
origin : candidate.origin ,
config : normalized ,
rootConfig : cfg ,
enabledByDefault : manifestRecord.enabledByDefault ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-04-02 20:57:14 +09:00
autoEnabledReason : formatAutoEnabledActivationReason ( autoEnabledReasons [ pluginId ] ) ,
} ) ;
2026-01-19 21:13:51 -06:00
const existingOrigin = seenIds . get ( pluginId ) ;
2026-01-17 09:33:56 +00:00
if ( existingOrigin ) {
const record = createPluginRecord ( {
2026-01-19 21:13:51 -06:00
id : pluginId ,
name : manifestRecord.name ? ? pluginId ,
description : manifestRecord.description ,
version : manifestRecord.version ,
2026-03-15 16:08:30 -07:00
format : manifestRecord.format ,
bundleFormat : manifestRecord.bundleFormat ,
bundleCapabilities : manifestRecord.bundleCapabilities ,
2026-01-17 09:33:56 +00:00
source : candidate.source ,
2026-03-15 19:06:11 -04:00
rootDir : candidate.rootDir ,
2026-01-17 09:33:56 +00:00
origin : candidate.origin ,
workspaceDir : candidate.workspaceDir ,
enabled : false ,
2026-04-02 20:57:14 +09:00
activationState ,
2026-01-19 21:13:51 -06:00
configSchema : Boolean ( manifestRecord . configSchema ) ,
2026-04-05 14:43:29 +01:00
contracts : manifestRecord.contracts ,
2026-01-17 09:33:56 +00:00
} ) ;
record . status = "disabled" ;
record . error = ` overridden by ${ existingOrigin } plugin ` ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , record . error ) ;
2026-01-17 09:33:56 +00:00
registry . plugins . push ( record ) ;
continue ;
}
2026-02-23 19:40:32 +00:00
const enableState = resolveEffectiveEnableState ( {
id : pluginId ,
origin : candidate.origin ,
config : normalized ,
rootConfig : cfg ,
2026-03-17 09:35:21 -07:00
enabledByDefault : manifestRecord.enabledByDefault ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-02-23 19:40:32 +00:00
} ) ;
2026-01-19 21:13:51 -06:00
const entry = normalized . entries [ pluginId ] ;
2026-01-11 12:11:12 +00:00
const record = createPluginRecord ( {
2026-01-19 21:13:51 -06:00
id : pluginId ,
name : manifestRecord.name ? ? pluginId ,
description : manifestRecord.description ,
version : manifestRecord.version ,
2026-03-15 16:08:30 -07:00
format : manifestRecord.format ,
bundleFormat : manifestRecord.bundleFormat ,
bundleCapabilities : manifestRecord.bundleCapabilities ,
2026-01-11 12:11:12 +00:00
source : candidate.source ,
2026-03-15 19:06:11 -04:00
rootDir : candidate.rootDir ,
2026-01-11 12:11:12 +00:00
origin : candidate.origin ,
workspaceDir : candidate.workspaceDir ,
enabled : enableState.enabled ,
2026-04-02 20:57:14 +09:00
activationState ,
2026-01-19 21:13:51 -06:00
configSchema : Boolean ( manifestRecord . configSchema ) ,
2026-04-05 14:43:29 +01:00
contracts : manifestRecord.contracts ,
2026-01-11 12:11:12 +00:00
} ) ;
2026-01-19 21:13:51 -06:00
record . kind = manifestRecord . kind ;
record . configUiHints = manifestRecord . configUiHints ;
record . configJsonSchema = manifestRecord . configSchema ;
2026-03-02 21:31:18 +00:00
const pushPluginLoadError = ( message : string ) = > {
record . status = "error" ;
record . error = message ;
2026-04-03 23:21:48 +09:00
record . failedAt = new Date ( ) ;
record . failurePhase = "validation" ;
2026-03-02 21:31:18 +00:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
registry . diagnostics . push ( {
level : "error" ,
pluginId : record.id ,
source : record.source ,
message : record.error ,
} ) ;
} ;
2026-01-11 12:11:12 +00:00
2026-03-15 16:17:24 -07:00
const registrationMode = enableState . enabled
2026-03-15 19:27:45 -07:00
? ! validateOnly &&
shouldLoadChannelPluginInSetupRuntime ( {
manifestChannels : manifestRecord.channels ,
setupSource : manifestRecord.setupSource ,
2026-03-16 13:55:53 +00:00
startupDeferConfiguredChannelFullLoadUntilAfterListen :
manifestRecord.startupDeferConfiguredChannelFullLoadUntilAfterListen ,
2026-03-15 19:27:45 -07:00
cfg ,
env ,
2026-03-16 13:30:24 +00:00
preferSetupRuntimeForChannelPlugins ,
2026-03-15 19:27:45 -07:00
} )
? "setup-runtime"
: "full"
2026-04-02 11:18:49 +09:00
: includeSetupOnlyChannelPlugins &&
! validateOnly &&
onlyPluginIdSet &&
manifestRecord . channels . length > 0
2026-03-15 16:17:24 -07:00
? "setup-only"
: null ;
if ( ! registrationMode ) {
2026-01-11 12:11:12 +00:00
record . status = "disabled" ;
record . error = enableState . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , enableState . reason ) ;
2026-01-11 12:11:12 +00:00
registry . plugins . push ( record ) ;
2026-01-19 21:13:51 -06:00
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-03-15 16:17:24 -07:00
if ( ! enableState . enabled ) {
record . status = "disabled" ;
record . error = enableState . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , enableState . reason ) ;
2026-03-15 16:17:24 -07:00
}
2026-01-19 21:13:51 -06:00
2026-03-15 16:08:30 -07:00
if ( record . format === "bundle" ) {
const unsupportedCapabilities = ( record . bundleCapabilities ? ? [ ] ) . filter (
( capability ) = >
capability !== "skills" &&
2026-03-16 21:46:05 -07:00
capability !== "mcpServers" &&
2026-03-15 16:08:30 -07:00
capability !== "settings" &&
! (
2026-03-18 00:11:37 -07:00
( capability === "commands" ||
capability === "agents" ||
capability === "outputStyles" ||
capability === "lspServers" ) &&
2026-03-15 16:08:30 -07:00
( record . bundleFormat === "claude" || record . bundleFormat === "cursor" )
) &&
2026-03-17 23:31:38 -07:00
! (
capability === "hooks" &&
( record . bundleFormat === "codex" || record . bundleFormat === "claude" )
) ,
2026-03-15 16:08:30 -07:00
) ;
for ( const capability of unsupportedCapabilities ) {
registry . diagnostics . push ( {
level : "warn" ,
pluginId : record.id ,
source : record.source ,
message : ` bundle capability detected but not wired into OpenClaw yet: ${ capability } ` ,
} ) ;
}
2026-03-16 21:46:05 -07:00
if (
enableState . enabled &&
record . rootDir &&
record . bundleFormat &&
( record . bundleCapabilities ? ? [ ] ) . includes ( "mcpServers" )
) {
const runtimeSupport = inspectBundleMcpRuntimeSupport ( {
pluginId : record.id ,
rootDir : record.rootDir ,
bundleFormat : record.bundleFormat ,
} ) ;
for ( const message of runtimeSupport . diagnostics ) {
registry . diagnostics . push ( {
level : "warn" ,
pluginId : record.id ,
source : record.source ,
message ,
} ) ;
}
if ( runtimeSupport . unsupportedServerNames . length > 0 ) {
registry . diagnostics . push ( {
level : "warn" ,
pluginId : record.id ,
source : record.source ,
message :
"bundle MCP servers use unsupported transports or incomplete configs " +
` (stdio only today): ${ runtimeSupport . unsupportedServerNames . join ( ", " ) } ` ,
} ) ;
}
}
2026-03-15 16:08:30 -07:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-03-04 01:19:17 -05:00
// Fast-path bundled memory plugins that are guaranteed disabled by slot policy.
// This avoids opening/importing heavy memory plugin modules that will never register.
2026-03-15 16:17:24 -07:00
if (
registrationMode === "full" &&
candidate . origin === "bundled" &&
2026-03-30 22:36:48 -06:00
hasKind ( manifestRecord . kind , "memory" )
2026-03-15 16:17:24 -07:00
) {
2026-03-04 01:19:17 -05:00
const earlyMemoryDecision = resolveMemorySlotDecision ( {
id : record.id ,
2026-03-30 22:36:48 -06:00
kind : manifestRecord.kind ,
2026-03-04 01:19:17 -05:00
slot : memorySlot ,
selectedId : selectedMemoryPluginId ,
} ) ;
if ( ! earlyMemoryDecision . enabled ) {
record . enabled = false ;
record . status = "disabled" ;
record . error = earlyMemoryDecision . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , earlyMemoryDecision . reason ) ;
2026-03-04 01:19:17 -05:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
}
2026-01-19 21:13:51 -06:00
if ( ! manifestRecord . configSchema ) {
2026-03-02 21:31:18 +00:00
pushPluginLoadError ( "missing config schema" ) ;
2026-01-11 12:11:12 +00:00
continue ;
}
2026-04-02 22:50:17 +09:00
if ( ! shouldLoadModules && registrationMode === "full" ) {
const memoryDecision = resolveMemorySlotDecision ( {
id : record.id ,
kind : record.kind ,
slot : memorySlot ,
selectedId : selectedMemoryPluginId ,
} ) ;
if ( ! memoryDecision . enabled ) {
record . enabled = false ;
record . status = "disabled" ;
record . error = memoryDecision . reason ;
markPluginActivationDisabled ( record , memoryDecision . reason ) ;
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
if ( memoryDecision . selected && hasKind ( record . kind , "memory" ) ) {
selectedMemoryPluginId = record . id ;
memorySlotMatched = true ;
record . memorySlotSelected = true ;
}
}
const validatedConfig = validatePluginConfig ( {
schema : manifestRecord.configSchema ,
cacheKey : manifestRecord.schemaCacheKey ,
value : entry?.config ,
} ) ;
if ( ! validatedConfig . ok ) {
logger . error ( ` [plugins] ${ record . id } invalid config: ${ validatedConfig . errors ? . join ( ", " ) } ` ) ;
pushPluginLoadError ( ` invalid config: ${ validatedConfig . errors ? . join ( ", " ) } ` ) ;
continue ;
}
if ( ! shouldLoadModules ) {
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-02-26 13:04:33 +01:00
const pluginRoot = safeRealpathOrResolve ( candidate . rootDir ) ;
2026-03-15 18:46:22 -07:00
const loadSource =
2026-03-15 19:27:45 -07:00
( registrationMode === "setup-only" || registrationMode === "setup-runtime" ) &&
manifestRecord . setupSource
2026-03-15 18:46:22 -07:00
? manifestRecord.setupSource
: candidate.source ;
2026-02-26 13:04:33 +01:00
const opened = openBoundaryFileSync ( {
2026-03-15 18:46:22 -07:00
absolutePath : loadSource ,
2026-02-26 13:04:33 +01:00
rootPath : pluginRoot ,
boundaryLabel : "plugin root" ,
2026-03-02 22:10:31 +00:00
rejectHardlinks : candidate.origin !== "bundled" ,
2026-02-26 12:40:57 +00:00
skipLexicalRootCheck : true ,
2026-02-26 13:04:33 +01:00
} ) ;
if ( ! opened . ok ) {
2026-03-02 21:31:18 +00:00
pushPluginLoadError ( "plugin entry path escapes plugin root or fails alias checks" ) ;
2026-02-19 15:34:58 +01:00
continue ;
}
2026-02-26 13:04:33 +01:00
const safeSource = opened . path ;
fs . closeSync ( opened . fd ) ;
2026-02-19 15:34:58 +01:00
2026-01-30 03:15:10 +01:00
let mod : OpenClawPluginModule | null = null ;
2026-01-11 12:11:12 +00:00
try {
2026-04-02 22:50:17 +09:00
// Track the plugin as imported once module evaluation begins. Top-level
// code may have already executed even if evaluation later throws.
recordImportedPluginId ( record . id ) ;
2026-03-18 16:24:37 +00:00
mod = getJiti ( safeSource ) ( safeSource ) as OpenClawPluginModule ;
2026-01-11 12:11:12 +00:00
} catch ( err ) {
2026-02-22 21:18:53 +00:00
recordPluginError ( {
logger ,
registry ,
record ,
seenIds ,
pluginId ,
origin : candidate.origin ,
2026-04-03 23:21:48 +09:00
phase : "load" ,
2026-02-22 21:18:53 +00:00
error : err ,
logPrefix : ` [plugins] ${ record . id } failed to load from ${ record . source } : ` ,
diagnosticMessagePrefix : "failed to load plugin: " ,
2026-01-11 12:11:12 +00:00
} ) ;
continue ;
}
2026-03-15 19:27:45 -07:00
if (
( registrationMode === "setup-only" || registrationMode === "setup-runtime" ) &&
manifestRecord . setupSource
) {
2026-03-15 18:46:22 -07:00
const setupRegistration = resolveSetupChannelRegistration ( mod ) ;
if ( setupRegistration . plugin ) {
if ( setupRegistration . plugin . id && setupRegistration . plugin . id !== record . id ) {
pushPluginLoadError (
` plugin id mismatch (config uses " ${ record . id } ", setup export uses " ${ setupRegistration . plugin . id } ") ` ,
) ;
continue ;
}
const api = createApi ( record , {
config : cfg ,
pluginConfig : { } ,
hookPolicy : entry?.hooks ,
registrationMode ,
} ) ;
2026-03-16 00:09:28 -07:00
api . registerChannel ( setupRegistration . plugin ) ;
2026-03-15 18:46:22 -07:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
}
2026-01-11 12:11:12 +00:00
const resolved = resolvePluginModuleExport ( mod ) ;
const definition = resolved . definition ;
const register = resolved . register ;
if ( definition ? . id && definition . id !== record . id ) {
2026-03-13 19:13:35 -07:00
pushPluginLoadError (
` plugin id mismatch (config uses " ${ record . id } ", export uses " ${ definition . id } ") ` ,
) ;
continue ;
2026-01-11 12:11:12 +00:00
}
record . name = definition ? . name ? ? record . name ;
record . description = definition ? . description ? ? record . description ;
record . version = definition ? . version ? ? record . version ;
2026-03-30 22:36:48 -06:00
const manifestKind = record . kind ;
const exportKind = definition ? . kind ;
if ( manifestKind && exportKind && ! kindsEqual ( manifestKind , exportKind ) ) {
2026-01-19 03:38:51 +00:00
registry . diagnostics . push ( {
2026-01-19 21:13:51 -06:00
level : "warn" ,
2026-01-19 03:38:51 +00:00
pluginId : record.id ,
source : record.source ,
2026-03-30 22:36:48 -06:00
message : ` plugin kind mismatch (manifest uses " ${ String ( manifestKind ) } ", export uses " ${ String ( exportKind ) } ") ` ,
2026-01-19 03:38:51 +00:00
} ) ;
}
2026-01-19 21:13:51 -06:00
record . kind = definition ? . kind ? ? record . kind ;
2026-01-19 03:38:51 +00:00
2026-03-30 22:36:48 -06:00
if ( hasKind ( record . kind , "memory" ) && memorySlot === record . id ) {
2026-01-18 02:12:01 +00:00
memorySlotMatched = true ;
}
2026-03-15 16:17:24 -07:00
if ( registrationMode === "full" ) {
const memoryDecision = resolveMemorySlotDecision ( {
id : record.id ,
kind : record.kind ,
slot : memorySlot ,
selectedId : selectedMemoryPluginId ,
} ) ;
2026-01-18 02:12:01 +00:00
2026-03-15 16:17:24 -07:00
if ( ! memoryDecision . enabled ) {
record . enabled = false ;
record . status = "disabled" ;
record . error = memoryDecision . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , memoryDecision . reason ) ;
2026-03-15 16:17:24 -07:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-01-18 02:12:01 +00:00
2026-03-30 22:36:48 -06:00
if ( memoryDecision . selected && hasKind ( record . kind , "memory" ) ) {
2026-03-15 16:17:24 -07:00
selectedMemoryPluginId = record . id ;
2026-03-30 22:36:48 -06:00
record . memorySlotSelected = true ;
2026-03-15 16:17:24 -07:00
}
2026-01-18 02:12:01 +00:00
}
2026-04-05 23:13:03 +01:00
if ( registrationMode === "full" ) {
if ( definition ? . reload ) {
registerReload ( record , definition . reload ) ;
}
for ( const nodeHostCommand of definition ? . nodeHostCommands ? ? [ ] ) {
registerNodeHostCommand ( record , nodeHostCommand ) ;
}
for ( const collector of definition ? . securityAuditCollectors ? ? [ ] ) {
registerSecurityAuditCollector ( record , collector ) ;
}
}
2026-01-19 03:38:51 +00:00
if ( validateOnly ) {
registry . plugins . push ( record ) ;
2026-01-19 21:13:51 -06:00
seenIds . set ( pluginId , candidate . origin ) ;
2026-01-19 03:38:51 +00:00
continue ;
}
2026-01-11 12:11:12 +00:00
if ( typeof register !== "function" ) {
2026-01-19 00:15:15 +00:00
logger . error ( ` [plugins] ${ record . id } missing register/activate export ` ) ;
2026-03-02 21:31:18 +00:00
pushPluginLoadError ( "plugin export missing register/activate" ) ;
2026-01-11 12:11:12 +00:00
continue ;
}
const api = createApi ( record , {
config : cfg ,
pluginConfig : validatedConfig.value ,
2026-03-05 18:15:54 -05:00
hookPolicy : entry?.hooks ,
2026-03-15 16:17:24 -07:00
registrationMode ,
2026-01-11 12:11:12 +00:00
} ) ;
2026-03-27 02:01:07 +00:00
const previousMemoryEmbeddingProviders = listRegisteredMemoryEmbeddingProviders ( ) ;
2026-03-26 21:30:39 +00:00
const previousMemoryFlushPlanResolver = getMemoryFlushPlanResolver ( ) ;
2026-03-21 20:20:45 +02:00
const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder ( ) ;
2026-03-26 22:29:38 +00:00
const previousMemoryRuntime = getMemoryRuntime ( ) ;
2026-01-11 12:11:12 +00:00
try {
const result = register ( api ) ;
2026-01-31 16:03:28 +09:00
if ( result && typeof result . then === "function" ) {
2026-01-11 12:11:12 +00:00
registry . diagnostics . push ( {
level : "warn" ,
pluginId : record.id ,
source : record.source ,
2026-01-14 14:31:43 +00:00
message : "plugin register returned a promise; async registration is ignored" ,
2026-01-11 12:11:12 +00:00
} ) ;
}
2026-03-21 20:20:45 +02:00
// Snapshot loads should not replace process-global runtime prompt state.
if ( ! shouldActivate ) {
2026-03-27 02:01:07 +00:00
restoreRegisteredMemoryEmbeddingProviders ( previousMemoryEmbeddingProviders ) ;
2026-03-26 22:14:33 +00:00
restoreMemoryPluginState ( {
promptBuilder : previousMemoryPromptBuilder ,
flushPlanResolver : previousMemoryFlushPlanResolver ,
2026-03-26 22:29:38 +00:00
runtime : previousMemoryRuntime ,
2026-03-26 22:14:33 +00:00
} ) ;
2026-03-21 20:20:45 +02:00
}
2026-01-11 12:11:12 +00:00
registry . plugins . push ( record ) ;
2026-01-19 21:13:51 -06:00
seenIds . set ( pluginId , candidate . origin ) ;
2026-01-11 12:11:12 +00:00
} catch ( err ) {
2026-03-27 02:01:07 +00:00
restoreRegisteredMemoryEmbeddingProviders ( previousMemoryEmbeddingProviders ) ;
2026-03-26 22:14:33 +00:00
restoreMemoryPluginState ( {
promptBuilder : previousMemoryPromptBuilder ,
flushPlanResolver : previousMemoryFlushPlanResolver ,
2026-03-26 22:29:38 +00:00
runtime : previousMemoryRuntime ,
2026-03-26 22:14:33 +00:00
} ) ;
2026-02-22 21:18:53 +00:00
recordPluginError ( {
logger ,
registry ,
record ,
seenIds ,
pluginId ,
origin : candidate.origin ,
2026-04-03 23:21:48 +09:00
phase : "register" ,
2026-02-22 21:18:53 +00:00
error : err ,
logPrefix : ` [plugins] ${ record . id } failed during register from ${ record . source } : ` ,
diagnosticMessagePrefix : "plugin failed during register: " ,
2026-01-11 12:11:12 +00:00
} ) ;
}
}
2026-03-16 07:52:08 +08:00
// Scoped snapshot loads may intentionally omit the configured memory plugin, so only
// emit the missing-memory diagnostic for full registry loads.
if ( ! onlyPluginIdSet && typeof memorySlot === "string" && ! memorySlotMatched ) {
2026-01-18 02:12:01 +00:00
registry . diagnostics . push ( {
level : "warn" ,
message : ` memory slot plugin not found or not marked as memory: ${ memorySlot } ` ,
} ) ;
}
2026-02-19 15:13:34 +01:00
warnAboutUntrackedLoadedPlugins ( {
registry ,
2026-02-19 15:24:02 +01:00
provenance ,
2026-04-01 04:10:30 +03:00
allowlist : normalized.allow ,
2026-04-05 13:08:43 -04:00
emitWarning : shouldActivate ,
2026-02-19 15:13:34 +01:00
logger ,
2026-03-12 15:31:31 +00:00
env ,
2026-02-19 15:13:34 +01:00
} ) ;
2026-03-19 16:04:19 -07:00
maybeThrowOnPluginLoadError ( registry , options . throwOnLoadError ) ;
2026-04-03 23:21:48 +09:00
if ( 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 list' for details. ` ,
) ;
}
}
2026-01-11 12:11:12 +00:00
if ( cacheEnabled ) {
2026-03-21 20:20:45 +02:00
setCachedPluginRegistry ( cacheKey , {
registry ,
2026-03-27 02:01:07 +00:00
memoryEmbeddingProviders : listRegisteredMemoryEmbeddingProviders ( ) ,
2026-03-26 21:30:39 +00:00
memoryFlushPlanResolver : getMemoryFlushPlanResolver ( ) ,
2026-03-21 20:20:45 +02:00
memoryPromptBuilder : getMemoryPromptSectionBuilder ( ) ,
2026-03-26 22:29:38 +00:00
memoryRuntime : getMemoryRuntime ( ) ,
2026-03-21 20:20:45 +02:00
} ) ;
2026-01-11 12:11:12 +00:00
}
2026-03-16 07:52:08 +08:00
if ( shouldActivate ) {
2026-04-05 11:03:54 +03:00
activatePluginRegistry ( registry , cacheKey , runtimeSubagentMode , options . workspaceDir ) ;
2026-03-16 07:52:08 +08:00
}
2026-01-11 12:11:12 +00:00
return registry ;
}
2026-02-26 13:04:33 +01:00
2026-03-29 18:49:57 -04:00
export async function loadOpenClawPluginCliRegistry (
options : PluginLoadOptions = { } ,
) : Promise < PluginRegistry > {
2026-04-03 23:36:52 +09:00
const { env , cfg , normalized , activationSource , autoEnabledReasons , onlyPluginIds , cacheKey } =
resolvePluginLoadCacheContext ( {
. . . options ,
activate : false ,
cache : false ,
} ) ;
2026-03-29 18:49:57 -04:00
const logger = options . logger ? ? defaultLogger ( ) ;
const onlyPluginIdSet = onlyPluginIds ? new Set ( onlyPluginIds ) : null ;
const getJiti = createPluginJitiLoader ( options ) ;
const { registry , registerCli } = createPluginRegistry ( {
logger ,
runtime : { } as PluginRuntime ,
coreGatewayHandlers : options.coreGatewayHandlers as Record < string , GatewayRequestHandler > ,
2026-03-30 17:40:29 +01:00
activateGlobalSideEffects : false ,
2026-03-29 18:49:57 -04:00
} ) ;
const discovery = discoverOpenClawPlugins ( {
workspaceDir : options.workspaceDir ,
extraPaths : normalized.loadPaths ,
cache : false ,
env ,
} ) ;
const manifestRegistry = loadPluginManifestRegistry ( {
config : cfg ,
workspaceDir : options.workspaceDir ,
cache : false ,
env ,
candidates : discovery.candidates ,
diagnostics : discovery.diagnostics ,
} ) ;
pushDiagnostics ( registry . diagnostics , manifestRegistry . diagnostics ) ;
warnWhenAllowlistIsOpen ( {
2026-04-05 13:08:43 -04:00
emitWarning : false ,
2026-03-29 18:49:57 -04:00
logger ,
pluginsEnabled : normalized.enabled ,
allow : normalized.allow ,
warningCacheKey : ` ${ cacheKey } ::cli-metadata ` ,
discoverablePlugins : manifestRegistry.plugins
. filter ( ( plugin ) = > ! onlyPluginIdSet || onlyPluginIdSet . has ( plugin . id ) )
. map ( ( plugin ) = > ( {
id : plugin.id ,
source : plugin.source ,
origin : plugin.origin ,
} ) ) ,
} ) ;
const provenance = buildProvenanceIndex ( {
config : cfg ,
normalizedLoadPaths : normalized.loadPaths ,
env ,
} ) ;
const manifestByRoot = new Map (
manifestRegistry . plugins . map ( ( record ) = > [ record . rootDir , record ] ) ,
) ;
const orderedCandidates = [ . . . discovery . candidates ] . toSorted ( ( left , right ) = > {
return compareDuplicateCandidateOrder ( {
left ,
right ,
manifestByRoot ,
provenance ,
env ,
} ) ;
} ) ;
const seenIds = new Map < string , PluginRecord [ "origin" ] > ( ) ;
const memorySlot = normalized . slots . memory ;
let selectedMemoryPluginId : string | null = null ;
for ( const candidate of orderedCandidates ) {
const manifestRecord = manifestByRoot . get ( candidate . rootDir ) ;
if ( ! manifestRecord ) {
continue ;
}
const pluginId = manifestRecord . id ;
2026-04-02 11:18:49 +09:00
if (
! matchesScopedPluginRequest ( {
onlyPluginIdSet ,
pluginId ,
} )
) {
2026-03-29 18:49:57 -04:00
continue ;
}
2026-04-02 20:57:14 +09:00
const activationState = resolveEffectivePluginActivationState ( {
id : pluginId ,
origin : candidate.origin ,
config : normalized ,
rootConfig : cfg ,
enabledByDefault : manifestRecord.enabledByDefault ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-04-02 20:57:14 +09:00
autoEnabledReason : formatAutoEnabledActivationReason ( autoEnabledReasons [ pluginId ] ) ,
} ) ;
2026-03-29 18:49:57 -04:00
const existingOrigin = seenIds . get ( pluginId ) ;
if ( existingOrigin ) {
const record = createPluginRecord ( {
id : pluginId ,
name : manifestRecord.name ? ? pluginId ,
description : manifestRecord.description ,
version : manifestRecord.version ,
format : manifestRecord.format ,
bundleFormat : manifestRecord.bundleFormat ,
bundleCapabilities : manifestRecord.bundleCapabilities ,
source : candidate.source ,
rootDir : candidate.rootDir ,
origin : candidate.origin ,
workspaceDir : candidate.workspaceDir ,
enabled : false ,
2026-04-02 20:57:14 +09:00
activationState ,
2026-03-29 18:49:57 -04:00
configSchema : Boolean ( manifestRecord . configSchema ) ,
2026-04-05 14:43:29 +01:00
contracts : manifestRecord.contracts ,
2026-03-29 18:49:57 -04:00
} ) ;
record . status = "disabled" ;
record . error = ` overridden by ${ existingOrigin } plugin ` ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , record . error ) ;
2026-03-29 18:49:57 -04:00
registry . plugins . push ( record ) ;
continue ;
}
const enableState = resolveEffectiveEnableState ( {
id : pluginId ,
origin : candidate.origin ,
config : normalized ,
rootConfig : cfg ,
enabledByDefault : manifestRecord.enabledByDefault ,
2026-04-03 23:36:52 +09:00
activationSource ,
2026-03-29 18:49:57 -04:00
} ) ;
const entry = normalized . entries [ pluginId ] ;
const record = createPluginRecord ( {
id : pluginId ,
name : manifestRecord.name ? ? pluginId ,
description : manifestRecord.description ,
version : manifestRecord.version ,
format : manifestRecord.format ,
bundleFormat : manifestRecord.bundleFormat ,
bundleCapabilities : manifestRecord.bundleCapabilities ,
source : candidate.source ,
rootDir : candidate.rootDir ,
origin : candidate.origin ,
workspaceDir : candidate.workspaceDir ,
enabled : enableState.enabled ,
2026-04-02 20:57:14 +09:00
activationState ,
2026-03-29 18:49:57 -04:00
configSchema : Boolean ( manifestRecord . configSchema ) ,
2026-04-05 14:43:29 +01:00
contracts : manifestRecord.contracts ,
2026-03-29 18:49:57 -04:00
} ) ;
record . kind = manifestRecord . kind ;
record . configUiHints = manifestRecord . configUiHints ;
record . configJsonSchema = manifestRecord . configSchema ;
const pushPluginLoadError = ( message : string ) = > {
record . status = "error" ;
record . error = message ;
2026-04-03 23:21:48 +09:00
record . failedAt = new Date ( ) ;
record . failurePhase = "validation" ;
2026-03-29 18:49:57 -04:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
registry . diagnostics . push ( {
level : "error" ,
pluginId : record.id ,
source : record.source ,
message : record.error ,
} ) ;
} ;
if ( ! enableState . enabled ) {
record . status = "disabled" ;
record . error = enableState . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , enableState . reason ) ;
2026-03-29 18:49:57 -04:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
if ( record . format === "bundle" ) {
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
if ( ! manifestRecord . configSchema ) {
pushPluginLoadError ( "missing config schema" ) ;
continue ;
}
const validatedConfig = validatePluginConfig ( {
schema : manifestRecord.configSchema ,
cacheKey : manifestRecord.schemaCacheKey ,
value : entry?.config ,
} ) ;
if ( ! validatedConfig . ok ) {
logger . error ( ` [plugins] ${ record . id } invalid config: ${ validatedConfig . errors ? . join ( ", " ) } ` ) ;
pushPluginLoadError ( ` invalid config: ${ validatedConfig . errors ? . join ( ", " ) } ` ) ;
continue ;
}
const pluginRoot = safeRealpathOrResolve ( candidate . rootDir ) ;
2026-04-04 02:35:23 +01:00
const cliMetadataSource = resolveCliMetadataEntrySource ( candidate . rootDir ) ;
const sourceForCliMetadata =
candidate . origin === "bundled" ? cliMetadataSource : ( cliMetadataSource ? ? candidate . source ) ;
if ( ! sourceForCliMetadata ) {
record . status = "loaded" ;
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-03-29 18:49:57 -04:00
const opened = openBoundaryFileSync ( {
2026-04-04 02:35:23 +01:00
absolutePath : sourceForCliMetadata ,
2026-03-29 18:49:57 -04:00
rootPath : pluginRoot ,
boundaryLabel : "plugin root" ,
rejectHardlinks : candidate.origin !== "bundled" ,
skipLexicalRootCheck : true ,
} ) ;
if ( ! opened . ok ) {
pushPluginLoadError ( "plugin entry path escapes plugin root or fails alias checks" ) ;
continue ;
}
const safeSource = opened . path ;
fs . closeSync ( opened . fd ) ;
let mod : OpenClawPluginModule | null = null ;
try {
mod = getJiti ( safeSource ) ( safeSource ) as OpenClawPluginModule ;
} catch ( err ) {
recordPluginError ( {
logger ,
registry ,
record ,
seenIds ,
pluginId ,
origin : candidate.origin ,
2026-04-03 23:21:48 +09:00
phase : "load" ,
2026-03-29 18:49:57 -04:00
error : err ,
logPrefix : ` [plugins] ${ record . id } failed to load from ${ record . source } : ` ,
diagnosticMessagePrefix : "failed to load plugin: " ,
} ) ;
continue ;
}
const resolved = resolvePluginModuleExport ( mod ) ;
const definition = resolved . definition ;
const register = resolved . register ;
if ( definition ? . id && definition . id !== record . id ) {
pushPluginLoadError (
` plugin id mismatch (config uses " ${ record . id } ", export uses " ${ definition . id } ") ` ,
) ;
continue ;
}
record . name = definition ? . name ? ? record . name ;
record . description = definition ? . description ? ? record . description ;
record . version = definition ? . version ? ? record . version ;
2026-03-30 22:36:48 -06:00
const manifestKind = record . kind ;
const exportKind = definition ? . kind ;
if ( manifestKind && exportKind && ! kindsEqual ( manifestKind , exportKind ) ) {
2026-03-29 18:49:57 -04:00
registry . diagnostics . push ( {
level : "warn" ,
pluginId : record.id ,
source : record.source ,
2026-03-30 22:36:48 -06:00
message : ` plugin kind mismatch (manifest uses " ${ String ( manifestKind ) } ", export uses " ${ String ( exportKind ) } ") ` ,
2026-03-29 18:49:57 -04:00
} ) ;
}
record . kind = definition ? . kind ? ? record . kind ;
2026-03-29 19:20:17 -04:00
const memoryDecision = resolveMemorySlotDecision ( {
id : record.id ,
kind : record.kind ,
slot : memorySlot ,
selectedId : selectedMemoryPluginId ,
} ) ;
if ( ! memoryDecision . enabled ) {
record . enabled = false ;
record . status = "disabled" ;
record . error = memoryDecision . reason ;
2026-04-02 20:57:14 +09:00
markPluginActivationDisabled ( record , memoryDecision . reason ) ;
2026-03-29 19:20:17 -04:00
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
continue ;
}
2026-03-30 22:36:48 -06:00
if ( memoryDecision . selected && hasKind ( record . kind , "memory" ) ) {
2026-03-29 19:20:17 -04:00
selectedMemoryPluginId = record . id ;
2026-03-30 22:36:48 -06:00
record . memorySlotSelected = true ;
2026-03-29 19:20:17 -04:00
}
2026-03-29 18:49:57 -04:00
if ( typeof register !== "function" ) {
logger . error ( ` [plugins] ${ record . id } missing register/activate export ` ) ;
pushPluginLoadError ( "plugin export missing register/activate" ) ;
continue ;
}
const api = buildPluginApi ( {
id : record.id ,
name : record.name ,
version : record.version ,
description : record.description ,
source : record.source ,
rootDir : record.rootDir ,
registrationMode : "cli-metadata" ,
config : cfg ,
pluginConfig : validatedConfig.value ,
runtime : { } as PluginRuntime ,
logger ,
resolvePath : ( input ) = > resolveUserPath ( input ) ,
handlers : {
registerCli : ( registrar , opts ) = > registerCli ( record , registrar , opts ) ,
} ,
} ) ;
try {
await register ( api ) ;
registry . plugins . push ( record ) ;
seenIds . set ( pluginId , candidate . origin ) ;
} catch ( err ) {
recordPluginError ( {
logger ,
registry ,
record ,
seenIds ,
pluginId ,
origin : candidate.origin ,
2026-04-03 23:21:48 +09:00
phase : "register" ,
2026-03-29 18:49:57 -04:00
error : err ,
logPrefix : ` [plugins] ${ record . id } failed during register from ${ record . source } : ` ,
diagnosticMessagePrefix : "plugin failed during register: " ,
} ) ;
}
}
return registry ;
}
2026-02-26 13:04:33 +01:00
function safeRealpathOrResolve ( value : string ) : string {
try {
return fs . realpathSync ( value ) ;
} catch {
return path . resolve ( value ) ;
}
}
2026-04-04 02:35:23 +01:00
function resolveCliMetadataEntrySource ( rootDir : string ) : string | null {
for ( const basename of CLI_METADATA_ENTRY_BASENAMES ) {
const candidate = path . join ( rootDir , basename ) ;
if ( fs . existsSync ( candidate ) ) {
return candidate ;
}
}
return null ;
}