Files
openclaw/src/plugins/loader.ts
2026-03-04 01:20:48 -05:00

812 lines
26 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createJiti } from "jiti";
import type { OpenClawConfig } from "../config/config.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveUserPath } from "../utils.js";
import { clearPluginCommands } from "./commands.js";
import {
applyTestPluginDefaults,
normalizePluginsConfig,
resolveEffectiveEnableState,
resolveMemorySlotDecision,
type NormalizedPluginsConfig,
} from "./config-state.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { initializeGlobalHookRunner } from "./hook-runner-global.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { isPathInside, safeStatSync } from "./path-safety.js";
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
import { setActivePluginRegistry } from "./runtime.js";
import { createPluginRuntime } from "./runtime/index.js";
import type { PluginRuntime } from "./runtime/types.js";
import { validateJsonSchemaValue } from "./schema-validator.js";
import type {
OpenClawPluginDefinition,
OpenClawPluginModule,
PluginDiagnostic,
PluginLogger,
} from "./types.js";
export type PluginLoadResult = PluginRegistry;
export type PluginLoadOptions = {
config?: OpenClawConfig;
workspaceDir?: string;
logger?: PluginLogger;
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
cache?: boolean;
mode?: "full" | "validate";
};
const registryCache = new Map<string, PluginRegistry>();
const defaultLogger = () => createSubsystemLogger("plugins");
const resolvePluginSdkAliasFile = (params: {
srcFile: string;
distFile: string;
modulePath?: string;
}): string | null => {
try {
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
const isProduction = process.env.NODE_ENV === "production";
const isTest = process.env.VITEST || process.env.NODE_ENV === "test";
const normalizedModulePath = modulePath.replace(/\\/g, "/");
const isDistRuntime = normalizedModulePath.includes("/dist/");
let cursor = path.dirname(modulePath);
for (let i = 0; i < 6; i += 1) {
const srcCandidate = path.join(cursor, "src", "plugin-sdk", params.srcFile);
const distCandidate = path.join(cursor, "dist", "plugin-sdk", params.distFile);
const orderedCandidates = isDistRuntime
? [distCandidate, srcCandidate]
: isProduction
? isTest
? [distCandidate, srcCandidate]
: [distCandidate]
: [srcCandidate, distCandidate];
for (const candidate of orderedCandidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
const parent = path.dirname(cursor);
if (parent === cursor) {
break;
}
cursor = parent;
}
} catch {
// ignore
}
return null;
};
const resolvePluginSdkAlias = (): string | null =>
resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" });
const resolvePluginSdkAccountIdAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "account-id.ts", distFile: "account-id.js" });
};
const resolvePluginSdkCoreAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "core.ts", distFile: "core.js" });
};
const resolvePluginSdkCompatAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "compat.ts", distFile: "compat.js" });
};
const resolvePluginSdkTelegramAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "telegram.ts", distFile: "telegram.js" });
};
const resolvePluginSdkDiscordAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "discord.ts", distFile: "discord.js" });
};
const resolvePluginSdkSlackAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "slack.ts", distFile: "slack.js" });
};
const resolvePluginSdkSignalAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "signal.ts", distFile: "signal.js" });
};
const resolvePluginSdkIMessageAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "imessage.ts", distFile: "imessage.js" });
};
const resolvePluginSdkWhatsAppAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "whatsapp.ts", distFile: "whatsapp.js" });
};
const resolvePluginSdkLineAlias = (): string | null => {
return resolvePluginSdkAliasFile({ srcFile: "line.ts", distFile: "line.js" });
};
export const __testing = {
resolvePluginSdkAliasFile,
};
function buildCacheKey(params: {
workspaceDir?: string;
plugins: NormalizedPluginsConfig;
}): string {
const workspaceKey = params.workspaceDir ? resolveUserPath(params.workspaceDir) : "";
return `${workspaceKey}::${JSON.stringify(params.plugins)}`;
}
function validatePluginConfig(params: {
schema?: Record<string, unknown>;
cacheKey?: string;
value?: unknown;
}): { ok: boolean; value?: Record<string, unknown>; errors?: string[] } {
const schema = params.schema;
if (!schema) {
return { ok: true, value: params.value as Record<string, unknown> | undefined };
}
const cacheKey = params.cacheKey ?? JSON.stringify(schema);
const result = validateJsonSchemaValue({
schema,
cacheKey,
value: params.value ?? {},
});
if (result.ok) {
return { ok: true, value: params.value as Record<string, unknown> | undefined };
}
return { ok: false, errors: result.errors.map((error) => error.text) };
}
function resolvePluginModuleExport(moduleExport: unknown): {
definition?: OpenClawPluginDefinition;
register?: OpenClawPluginDefinition["register"];
} {
const resolved =
moduleExport &&
typeof moduleExport === "object" &&
"default" in (moduleExport as Record<string, unknown>)
? (moduleExport as { default: unknown }).default
: moduleExport;
if (typeof resolved === "function") {
return {
register: resolved as OpenClawPluginDefinition["register"],
};
}
if (resolved && typeof resolved === "object") {
const def = resolved as OpenClawPluginDefinition;
const register = def.register ?? def.activate;
return { definition: def, register };
}
return {};
}
function createPluginRecord(params: {
id: string;
name?: string;
description?: string;
version?: string;
source: string;
origin: PluginRecord["origin"];
workspaceDir?: string;
enabled: boolean;
configSchema: boolean;
}): PluginRecord {
return {
id: params.id,
name: params.name ?? params.id,
description: params.description,
version: params.version,
source: params.source,
origin: params.origin,
workspaceDir: params.workspaceDir,
enabled: params.enabled,
status: params.enabled ? "loaded" : "disabled",
toolNames: [],
hookNames: [],
channelIds: [],
providerIds: [],
gatewayMethods: [],
cliCommands: [],
services: [],
commands: [],
httpRoutes: 0,
hookCount: 0,
configSchema: params.configSchema,
configUiHints: undefined,
configJsonSchema: undefined,
};
}
function recordPluginError(params: {
logger: PluginLogger;
registry: PluginRegistry;
record: PluginRecord;
seenIds: Map<string, PluginRecord["origin"]>;
pluginId: string;
origin: PluginRecord["origin"];
error: unknown;
logPrefix: string;
diagnosticMessagePrefix: string;
}) {
const errorText = String(params.error);
params.logger.error(`${params.logPrefix}${errorText}`);
params.record.status = "error";
params.record.error = errorText;
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,
message: `${params.diagnosticMessagePrefix}${errorText}`,
});
}
function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnostic[]) {
diagnostics.push(...append);
}
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: [] };
}
function addPathToMatcher(matcher: PathMatcher, rawPath: string): void {
const trimmed = rawPath.trim();
if (!trimmed) {
return;
}
const resolved = resolveUserPath(trimmed);
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);
}
function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean {
if (matcher.exact.has(sourcePath)) {
return true;
}
return matcher.dirs.some((dirPath) => isPathInside(dirPath, sourcePath));
}
function buildProvenanceIndex(params: {
config: OpenClawConfig;
normalizedLoadPaths: string[];
}): PluginProvenanceIndex {
const loadPathMatcher = createPathMatcher();
for (const loadPath of params.normalizedLoadPaths) {
addPathToMatcher(loadPathMatcher, loadPath);
}
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) {
addPathToMatcher(rule.matcher, trackedPath);
}
}
installRules.set(pluginId, rule);
}
return { loadPathMatcher, installRules };
}
function isTrackedByProvenance(params: {
pluginId: string;
source: string;
index: PluginProvenanceIndex;
}): boolean {
const sourcePath = resolveUserPath(params.source);
const installRule = params.index.installRules.get(params.pluginId);
if (installRule) {
if (installRule.trackedWithoutPaths) {
return true;
}
if (matchesPathMatcher(installRule.matcher, sourcePath)) {
return true;
}
}
return matchesPathMatcher(params.index.loadPathMatcher, sourcePath);
}
function warnWhenAllowlistIsOpen(params: {
logger: PluginLogger;
pluginsEnabled: boolean;
allow: string[];
discoverablePlugins: Array<{ id: string; source: string; origin: PluginRecord["origin"] }>;
}) {
if (!params.pluginsEnabled) {
return;
}
if (params.allow.length > 0) {
return;
}
const nonBundled = params.discoverablePlugins.filter((entry) => entry.origin !== "bundled");
if (nonBundled.length === 0) {
return;
}
const preview = nonBundled
.slice(0, 6)
.map((entry) => `${entry.id} (${entry.source})`)
.join(", ");
const extra = nonBundled.length > 6 ? ` (+${nonBundled.length - 6} more)` : "";
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;
provenance: PluginProvenanceIndex;
logger: PluginLogger;
}) {
for (const plugin of params.registry.plugins) {
if (plugin.status !== "loaded" || plugin.origin === "bundled") {
continue;
}
if (
isTrackedByProvenance({
pluginId: plugin.id,
source: plugin.source,
index: params.provenance,
})
) {
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,
});
params.logger.warn(`[plugins] ${plugin.id}: ${message} (${plugin.source})`);
}
}
function activatePluginRegistry(registry: PluginRegistry, cacheKey: string): void {
setActivePluginRegistry(registry, cacheKey);
initializeGlobalHookRunner(registry);
}
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
// Test env: default-disable plugins unless explicitly configured.
// This keeps unit/gateway suites fast and avoids loading heavyweight plugin deps by accident.
const cfg = applyTestPluginDefaults(options.config ?? {}, process.env);
const logger = options.logger ?? defaultLogger();
const validateOnly = options.mode === "validate";
const normalized = normalizePluginsConfig(cfg.plugins);
const cacheKey = buildCacheKey({
workspaceDir: options.workspaceDir,
plugins: normalized,
});
const cacheEnabled = options.cache !== false;
if (cacheEnabled) {
const cached = registryCache.get(cacheKey);
if (cached) {
activatePluginRegistry(cached, cacheKey);
return cached;
}
}
// Clear previously registered plugin commands before reloading
clearPluginCommands();
// Lazily initialize the runtime so startup paths that discover/skip plugins do
// not eagerly load every channel runtime dependency.
let resolvedRuntime: PluginRuntime | null = null;
const resolveRuntime = (): PluginRuntime => {
resolvedRuntime ??= createPluginRuntime();
return resolvedRuntime;
};
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) {
return Reflect.has(resolveRuntime(), prop);
},
ownKeys() {
return Reflect.ownKeys(resolveRuntime() as object);
},
getOwnPropertyDescriptor(_target, prop) {
return Reflect.getOwnPropertyDescriptor(resolveRuntime() as object, prop);
},
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);
},
});
const { registry, createApi } = createPluginRegistry({
logger,
runtime,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
});
const discovery = discoverOpenClawPlugins({
workspaceDir: options.workspaceDir,
extraPaths: normalized.loadPaths,
cache: options.cache,
});
const manifestRegistry = loadPluginManifestRegistry({
config: cfg,
workspaceDir: options.workspaceDir,
cache: options.cache,
candidates: discovery.candidates,
diagnostics: discovery.diagnostics,
});
pushDiagnostics(registry.diagnostics, manifestRegistry.diagnostics);
warnWhenAllowlistIsOpen({
logger,
pluginsEnabled: normalized.enabled,
allow: normalized.allow,
discoverablePlugins: manifestRegistry.plugins.map((plugin) => ({
id: plugin.id,
source: plugin.source,
origin: plugin.origin,
})),
});
const provenance = buildProvenanceIndex({
config: cfg,
normalizedLoadPaths: normalized.loadPaths,
});
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
let jitiLoader: ReturnType<typeof createJiti> | null = null;
const getJiti = () => {
if (jitiLoader) {
return jitiLoader;
}
const pluginSdkAlias = resolvePluginSdkAlias();
const pluginSdkAccountIdAlias = resolvePluginSdkAccountIdAlias();
const pluginSdkCoreAlias = resolvePluginSdkCoreAlias();
const pluginSdkCompatAlias = resolvePluginSdkCompatAlias();
const pluginSdkTelegramAlias = resolvePluginSdkTelegramAlias();
const pluginSdkDiscordAlias = resolvePluginSdkDiscordAlias();
const pluginSdkSlackAlias = resolvePluginSdkSlackAlias();
const pluginSdkSignalAlias = resolvePluginSdkSignalAlias();
const pluginSdkIMessageAlias = resolvePluginSdkIMessageAlias();
const pluginSdkWhatsAppAlias = resolvePluginSdkWhatsAppAlias();
const pluginSdkLineAlias = resolvePluginSdkLineAlias();
const aliasMap = {
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...(pluginSdkCoreAlias ? { "openclaw/plugin-sdk/core": pluginSdkCoreAlias } : {}),
...(pluginSdkCompatAlias ? { "openclaw/plugin-sdk/compat": pluginSdkCompatAlias } : {}),
...(pluginSdkTelegramAlias ? { "openclaw/plugin-sdk/telegram": pluginSdkTelegramAlias } : {}),
...(pluginSdkDiscordAlias ? { "openclaw/plugin-sdk/discord": pluginSdkDiscordAlias } : {}),
...(pluginSdkSlackAlias ? { "openclaw/plugin-sdk/slack": pluginSdkSlackAlias } : {}),
...(pluginSdkSignalAlias ? { "openclaw/plugin-sdk/signal": pluginSdkSignalAlias } : {}),
...(pluginSdkIMessageAlias ? { "openclaw/plugin-sdk/imessage": pluginSdkIMessageAlias } : {}),
...(pluginSdkWhatsAppAlias ? { "openclaw/plugin-sdk/whatsapp": pluginSdkWhatsAppAlias } : {}),
...(pluginSdkLineAlias ? { "openclaw/plugin-sdk/line": pluginSdkLineAlias } : {}),
...(pluginSdkAccountIdAlias
? { "openclaw/plugin-sdk/account-id": pluginSdkAccountIdAlias }
: {}),
};
jitiLoader = createJiti(import.meta.url, {
interopDefault: true,
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
...(Object.keys(aliasMap).length > 0
? {
alias: aliasMap,
}
: {}),
});
return jitiLoader;
};
const manifestByRoot = new Map(
manifestRegistry.plugins.map((record) => [record.rootDir, record]),
);
const seenIds = new Map<string, PluginRecord["origin"]>();
const memorySlot = normalized.slots.memory;
let selectedMemoryPluginId: string | null = null;
let memorySlotMatched = false;
for (const candidate of discovery.candidates) {
const manifestRecord = manifestByRoot.get(candidate.rootDir);
if (!manifestRecord) {
continue;
}
const pluginId = manifestRecord.id;
const existingOrigin = seenIds.get(pluginId);
if (existingOrigin) {
const record = createPluginRecord({
id: pluginId,
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
source: candidate.source,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
enabled: false,
configSchema: Boolean(manifestRecord.configSchema),
});
record.status = "disabled";
record.error = `overridden by ${existingOrigin} plugin`;
registry.plugins.push(record);
continue;
}
const enableState = resolveEffectiveEnableState({
id: pluginId,
origin: candidate.origin,
config: normalized,
rootConfig: cfg,
});
const entry = normalized.entries[pluginId];
const record = createPluginRecord({
id: pluginId,
name: manifestRecord.name ?? pluginId,
description: manifestRecord.description,
version: manifestRecord.version,
source: candidate.source,
origin: candidate.origin,
workspaceDir: candidate.workspaceDir,
enabled: enableState.enabled,
configSchema: Boolean(manifestRecord.configSchema),
});
record.kind = manifestRecord.kind;
record.configUiHints = manifestRecord.configUiHints;
record.configJsonSchema = manifestRecord.configSchema;
const pushPluginLoadError = (message: string) => {
record.status = "error";
record.error = message;
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;
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
// Fast-path bundled memory plugins that are guaranteed disabled by slot policy.
// This avoids opening/importing heavy memory plugin modules that will never register.
if (candidate.origin === "bundled" && manifestRecord.kind === "memory") {
const earlyMemoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: "memory",
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!earlyMemoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = earlyMemoryDecision.reason;
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
}
if (!manifestRecord.configSchema) {
pushPluginLoadError("missing config schema");
continue;
}
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
const opened = openBoundaryFileSync({
absolutePath: candidate.source,
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) as OpenClawPluginModule;
} catch (err) {
recordPluginError({
logger,
registry,
record,
seenIds,
pluginId,
origin: candidate.origin,
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) {
registry.diagnostics.push({
level: "warn",
pluginId: record.id,
source: record.source,
message: `plugin id mismatch (config uses "${record.id}", export uses "${definition.id}")`,
});
}
record.name = definition?.name ?? record.name;
record.description = definition?.description ?? record.description;
record.version = definition?.version ?? record.version;
const manifestKind = record.kind as string | undefined;
const exportKind = definition?.kind as string | undefined;
if (manifestKind && exportKind && exportKind !== manifestKind) {
registry.diagnostics.push({
level: "warn",
pluginId: record.id,
source: record.source,
message: `plugin kind mismatch (manifest uses "${manifestKind}", export uses "${exportKind}")`,
});
}
record.kind = definition?.kind ?? record.kind;
if (record.kind === "memory" && memorySlot === record.id) {
memorySlotMatched = true;
}
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;
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (memoryDecision.selected && record.kind === "memory") {
selectedMemoryPluginId = record.id;
}
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 (validateOnly) {
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (typeof register !== "function") {
logger.error(`[plugins] ${record.id} missing register/activate export`);
pushPluginLoadError("plugin export missing register/activate");
continue;
}
const api = createApi(record, {
config: cfg,
pluginConfig: validatedConfig.value,
});
try {
const result = register(api);
if (result && typeof result.then === "function") {
registry.diagnostics.push({
level: "warn",
pluginId: record.id,
source: record.source,
message: "plugin register returned a promise; async registration is ignored",
});
}
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
} catch (err) {
recordPluginError({
logger,
registry,
record,
seenIds,
pluginId,
origin: candidate.origin,
error: err,
logPrefix: `[plugins] ${record.id} failed during register from ${record.source}: `,
diagnosticMessagePrefix: "plugin failed during register: ",
});
}
}
if (typeof memorySlot === "string" && !memorySlotMatched) {
registry.diagnostics.push({
level: "warn",
message: `memory slot plugin not found or not marked as memory: ${memorySlot}`,
});
}
warnAboutUntrackedLoadedPlugins({
registry,
provenance,
logger,
});
if (cacheEnabled) {
registryCache.set(cacheKey, registry);
}
activatePluginRegistry(registry, cacheKey);
return registry;
}
function safeRealpathOrResolve(value: string): string {
try {
return fs.realpathSync(value);
} catch {
return path.resolve(value);
}
}