Files
openclaw/src/plugins/loader-runtime-load.ts
Peter Steinberger 89db948bb6 refactor(core): consolidate plugin install pipeline, migrate worktree ledger, carry prepared runtime metadata (#114749)
* refactor(plugins): consolidate install execution

* fix(worktrees): retire pre-ledger registry rows

Release note: Very old managed worktrees without provisioned-file ledgers are retired by openclaw doctor --fix and provision fresh on next use; existing checkout and branch data remain untouched.

* perf(plugins): reuse prepared runtime metadata

* fix(plugins): restore shared install result typing

* refactor(plugins): keep install helpers private
2026-07-27 19:34:01 -04:00

265 lines
9.9 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,
clearActivatedPluginRuntimeState,
createPluginLoaderLogger,
maybeThrowOnPluginLoadError,
resolveAuthorizedDreamingSidecar,
} from "./loader-shared.js";
import type { PluginLoadOptions } from "./loader-types.js";
import {
createPluginRegistrationTransaction,
restorePluginProcessGlobalState,
snapshotPluginProcessGlobalState,
} from "./plugin-registration-transaction.js";
import { createPluginIdScopeSet, normalizePluginIdScope } from "./plugin-scope.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import { createPluginRegistry, type PluginRegistry } from "./registry.js";
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
const requestedOnlyPluginIds = normalizePluginIdScope(options.onlyPluginIds);
const requestedOnlyPluginIdSet = createPluginIdScopeSet(requestedOnlyPluginIds);
if (requestedOnlyPluginIdSet && requestedOnlyPluginIdSet.size === 0) {
const emptyRegistry = createEmptyPluginRegistry();
if (options.activate !== false) {
clearActivatedPluginRuntimeState();
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({
cacheKey: context.cacheKey,
onlyPluginIds: context.onlyPluginIds,
runtimeSubagentMode: context.runtimeSubagentMode,
options,
});
if (cached) {
if (context.shouldActivate) {
restorePluginProcessGlobalState(cached.state.processGlobalState);
activatePluginRegistry(
cached.state.registry,
cached.cacheKey,
cached.runtimeSubagentMode,
options.workspaceDir,
);
}
return cached.state.registry;
}
}
pluginLoaderCacheState.beginLoad(context.cacheKey);
let registryBuilder: ReturnType<typeof createPluginRegistry> | undefined;
const activatingLoadTransaction = context.shouldActivate
? createPluginRegistrationTransaction({
rollbackGlobalSideEffects: () => {
const loadedPluginIds = (registryBuilder?.registry.plugins ?? [])
.filter((plugin) => plugin.status === "loaded")
.map((plugin) => plugin.id);
for (const pluginId of loadedPluginIds.toReversed()) {
registryBuilder?.rollbackPluginGlobalSideEffects(pluginId);
}
},
})
: null;
try {
// Snapshot loads must not wipe global state registered by the active plugin set.
if (context.shouldActivate) {
clearActivatedPluginRuntimeState();
}
// Module and runtime loading stay lazy for discovery-only or disabled-plugin paths.
const loadPluginModule = createPluginModuleLoader({
devSourceRoot: context.devSourceRoot,
pluginSdkResolution: options.pluginSdkResolution,
});
const runtime = createLazyPluginRuntime({
devSourceRoot: context.devSourceRoot,
pluginSdkResolution: options.pluginSdkResolution,
runtimeOptions: options.runtimeOptions,
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 (cacheEnabled) {
setCachedPluginRegistry(
context.cacheKey,
{
registry,
processGlobalState: snapshotPluginProcessGlobalState(),
},
context.onlyPluginIds,
);
}
if (context.shouldActivate) {
// Activation installs the new registry before initializing its hook runner. Commit the
// rollback first so an activation throw cannot restore old globals under the new registry.
activatingLoadTransaction?.commit({ activate: true });
activatePluginRegistry(
registry,
context.cacheKey,
context.runtimeSubagentMode,
options.workspaceDir,
);
}
return registry;
} catch (error) {
activatingLoadTransaction?.rollback();
throw error;
} finally {
pluginLoaderCacheState.finishLoad(context.cacheKey);
}
}
export { clearActivatedPluginRuntimeState } from "./loader-shared.js";