mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-10 08:41:13 +00:00
* feat(plugins): support multi-kind plugins for dual slot ownership * fix: address review feedback on multi-kind plugin support - Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts (fixes order-sensitive JSON.stringify for arrays) - Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts (removes hardcoded ternary that would break for future slot types) - Use shared hasKind() helper in config-state.ts instead of inline logic * fix: don't disable dual-kind plugin that still owns another slot When a new plugin takes over one slot, a dual-kind plugin that still owns the other slot must not be disabled — otherwise context engine resolution fails at runtime. * fix: exempt dual-kind plugins from memory slot disablement A plugin with kind: ["memory", "context-engine"] must stay enabled even when it loses the memory slot, so its context engine role can still load. * fix: address remaining review feedback - Pass manifest kind (not hardcoded "memory") in early memory gating - Extract kindsEqual() helper for DRY kind comparison in loader.ts - Narrow slotKeyForPluginKind back to single PluginKind with JSDoc - Reject empty array in parsePluginKind - Add kindsEqual tests * fix: use toSorted() instead of sort() per lint rules * plugins: include default slot ownership in disable checks and gate dual-kind memory registration
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import {
|
|
normalizePluginsConfig,
|
|
resolveEffectiveEnableState,
|
|
resolveMemorySlotDecision,
|
|
} from "../plugins/config-state.js";
|
|
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
|
import { hasKind } from "../plugins/slots.js";
|
|
import { isPathInsideWithRealpath } from "../security/scan-paths.js";
|
|
|
|
const log = createSubsystemLogger("hooks");
|
|
|
|
export type PluginHookDirEntry = {
|
|
dir: string;
|
|
pluginId: string;
|
|
};
|
|
|
|
export function resolvePluginHookDirs(params: {
|
|
workspaceDir: string | undefined;
|
|
config?: OpenClawConfig;
|
|
}): PluginHookDirEntry[] {
|
|
const workspaceDir = (params.workspaceDir ?? "").trim();
|
|
if (!workspaceDir) {
|
|
return [];
|
|
}
|
|
const registry = loadPluginManifestRegistry({
|
|
workspaceDir,
|
|
config: params.config,
|
|
// Hook discovery should reflect freshly written bundle manifests immediately.
|
|
cache: false,
|
|
});
|
|
if (registry.plugins.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
|
const memorySlot = normalizedPlugins.slots.memory;
|
|
let selectedMemoryPluginId: string | null = null;
|
|
const seen = new Set<string>();
|
|
const resolved: PluginHookDirEntry[] = [];
|
|
|
|
for (const record of registry.plugins) {
|
|
if (!record.hooks || record.hooks.length === 0) {
|
|
continue;
|
|
}
|
|
const enableState = resolveEffectiveEnableState({
|
|
id: record.id,
|
|
origin: record.origin,
|
|
config: normalizedPlugins,
|
|
rootConfig: params.config,
|
|
});
|
|
if (!enableState.enabled) {
|
|
continue;
|
|
}
|
|
|
|
const memoryDecision = resolveMemorySlotDecision({
|
|
id: record.id,
|
|
kind: record.kind,
|
|
slot: memorySlot,
|
|
selectedId: selectedMemoryPluginId,
|
|
});
|
|
if (!memoryDecision.enabled) {
|
|
continue;
|
|
}
|
|
if (memoryDecision.selected && hasKind(record.kind, "memory")) {
|
|
selectedMemoryPluginId = record.id;
|
|
}
|
|
|
|
for (const raw of record.hooks) {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
const candidate = path.resolve(record.rootDir, trimmed);
|
|
if (!fs.existsSync(candidate)) {
|
|
log.warn(`plugin hook path not found (${record.id}): ${candidate}`);
|
|
continue;
|
|
}
|
|
if (!isPathInsideWithRealpath(record.rootDir, candidate, { requireRealpath: true })) {
|
|
log.warn(`plugin hook path escapes plugin root (${record.id}): ${candidate}`);
|
|
continue;
|
|
}
|
|
if (seen.has(candidate)) {
|
|
continue;
|
|
}
|
|
seen.add(candidate);
|
|
resolved.push({
|
|
dir: candidate,
|
|
pluginId: record.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
return resolved;
|
|
}
|