Files
openclaw/src/plugins/loader-runtime-load.ts
T

299 lines
11 KiB
TypeScript

import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { normalizeAgentToolResultMiddlewareRuntimeIds } from "./agent-tool-result-middleware.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<typeof createPluginModuleLoader>[0],
"tryNative" | "loaderFilename" | "installNativeSdkResolver"
>;
type InternalPluginLoadOverrides = {
moduleLoader: PluginModuleLoaderOverrides;
runtime: Pick<PluginRuntime, "config">;
};
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<typeof createPluginRegistry> | 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<string, GatewayRequestHandler>,
...(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({
registry,
provenance,
allowlist: context.normalized.allow,
emitWarning: context.shouldActivate,
logger,
env: context.env,
});
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 <id> --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);
}
}