import fs from "node:fs"; import path from "node:path"; import type { OpenClawConfig, HookConfig } from "../config/config.js"; import type { HookEligibilityContext, HookEntry } from "./types.js"; import { resolveHookKey } from "./frontmatter.js"; const DEFAULT_CONFIG_VALUES: Record = { "browser.enabled": true, "browser.evaluateEnabled": true, "workspace.dir": true, }; function isTruthy(value: unknown): boolean { if (value === undefined || value === null) { return false; } if (typeof value === "boolean") { return value; } if (typeof value === "number") { return value !== 0; } if (typeof value === "string") { return value.trim().length > 0; } return true; } export function resolveConfigPath(config: OpenClawConfig | undefined, pathStr: string) { const parts = pathStr.split(".").filter(Boolean); let current: unknown = config; for (const part of parts) { if (typeof current !== "object" || current === null) { return undefined; } current = (current as Record)[part]; } return current; } export function isConfigPathTruthy(config: OpenClawConfig | undefined, pathStr: string): boolean { const value = resolveConfigPath(config, pathStr); if (value === undefined && pathStr in DEFAULT_CONFIG_VALUES) { return DEFAULT_CONFIG_VALUES[pathStr]; } return isTruthy(value); } export function resolveHookConfig( config: OpenClawConfig | undefined, hookKey: string, ): HookConfig | undefined { const hooks = config?.hooks?.internal?.entries; if (!hooks || typeof hooks !== "object") { return undefined; } const entry = (hooks as Record)[hookKey]; if (!entry || typeof entry !== "object") { return undefined; } return entry; } export function resolveRuntimePlatform(): string { return process.platform; } export function hasBinary(bin: string): boolean { const pathEnv = process.env.PATH ?? ""; const parts = pathEnv.split(path.delimiter).filter(Boolean); for (const part of parts) { const candidate = path.join(part, bin); try { fs.accessSync(candidate, fs.constants.X_OK); return true; } catch { // keep scanning } } return false; } export function shouldIncludeHook(params: { entry: HookEntry; config?: OpenClawConfig; eligibility?: HookEligibilityContext; }): boolean { const { entry, config, eligibility } = params; const hookKey = resolveHookKey(entry.hook.name, entry); const hookConfig = resolveHookConfig(config, hookKey); const pluginManaged = entry.hook.source === "openclaw-plugin"; const osList = entry.metadata?.os ?? []; const remotePlatforms = eligibility?.remote?.platforms ?? []; // Check if explicitly disabled if (!pluginManaged && hookConfig?.enabled === false) { return false; } // Check OS requirement if ( osList.length > 0 && !osList.includes(resolveRuntimePlatform()) && !remotePlatforms.some((platform) => osList.includes(platform)) ) { return false; } // If marked as 'always', bypass all other checks if (entry.metadata?.always === true) { return true; } // Check required binaries (all must be present) const requiredBins = entry.metadata?.requires?.bins ?? []; if (requiredBins.length > 0) { for (const bin of requiredBins) { if (hasBinary(bin)) { continue; } if (eligibility?.remote?.hasBin?.(bin)) { continue; } return false; } } // Check anyBins (at least one must be present) const requiredAnyBins = entry.metadata?.requires?.anyBins ?? []; if (requiredAnyBins.length > 0) { const anyFound = requiredAnyBins.some((bin) => hasBinary(bin)) || eligibility?.remote?.hasAnyBin?.(requiredAnyBins); if (!anyFound) { return false; } } // Check required environment variables const requiredEnv = entry.metadata?.requires?.env ?? []; if (requiredEnv.length > 0) { for (const envName of requiredEnv) { if (process.env[envName]) { continue; } if (hookConfig?.env?.[envName]) { continue; } return false; } } // Check required config paths const requiredConfig = entry.metadata?.requires?.config ?? []; if (requiredConfig.length > 0) { for (const configPath of requiredConfig) { if (!isConfigPathTruthy(config, configPath)) { return false; } } } return true; }