mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 01:51:33 +00:00
* 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
265 lines
9.9 KiB
TypeScript
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";
|