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 { 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, }); 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(); 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 && 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; }