openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
115 lines (114 loc) • 5.63 kB
JavaScript
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs";
import { a as openRootFile } from "./boundary-file-read-CCth2Hwg.mjs";
import { o as safeRealpathSync } from "./boundary-path-CAv_plX7.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { n as sanitizeForLog } from "./ansi-6Z0Nu6M4.mjs";
import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs";
import { c as registerInternalHook, d as unregisterInternalHook, l as setInternalHooksEnabled } from "./internal-hooks-DIPAwHyY.mjs";
import { n as resolveFunctionModuleExport } from "./module-loader-CgS9w1oK.mjs";
import { t as isKnownInternalHookEventKey } from "./internal-hook-types-Deg4lhm7.mjs";
import { n as prepareWorkspaceHookEntries } from "./workspace-DtwjBExX.mjs";
import { n as isHookNameSelected, r as resolveInternalHookSelection, t as isHookLoadable } from "./configured-9et3h1pT.mjs";
import fs from "node:fs";
import { pathToFileURL } from "node:url";
//#region src/hooks/import-url.ts
function buildImportUrl(handlerPath, source) {
const base = pathToFileURL(handlerPath).href;
if (source === "openclaw-bundled") return base;
try {
const { ctimeMs, mtimeMs, size } = fs.statSync(handlerPath);
return `${base}?t=${mtimeMs}&c=${ctimeMs}&s=${size}`;
} catch {
return `${base}?t=${Date.now()}`;
}
}
//#endregion
//#region src/hooks/loader.ts
const log = createSubsystemLogger("hooks:loader");
const hookOwner = resolveGlobalSingleton(Symbol.for("openclaw.loadedInternalHookRegistrations"), () => ({ generation: { registrations: [] } }), () => resetLoadedInternalHooks());
function resetLoadedInternalHooks() {
for (const { event, handler } of hookOwner.generation.registrations) unregisterInternalHook(event, handler);
hookOwner.generation = { registrations: [] };
}
/** Imports candidate handlers without publishing registrations or changing the dispatch gate. */
async function prepareInternalHooks(cfg, workspaceDir, opts) {
const enabled = cfg.hooks?.internal?.enabled !== false;
const previousGeneration = hookOwner.generation;
const registrations = [];
let loadedCount = 0;
const selection = resolveInternalHookSelection(cfg);
const shouldLoadHook = (entry) => isHookLoadable({
entry,
config: cfg,
names: selection.names
});
const discovery = selection.configured ? prepareWorkspaceHookEntries(workspaceDir, {
config: cfg,
managedHooksDir: opts?.managedHooksDir,
bundledHooksDir: opts?.bundledHooksDir,
...opts?.failureMode !== "best-effort" ? {
requireValidHook: shouldLoadHook,
previousSources: previousGeneration.discovery?.sources.filter((entry) => !isHookNameSelected(previousGeneration.discovery?.declaredNames, entry) || isHookNameSelected(selection.declaredNames, entry))
} : {}
}) : {
entries: [],
sources: []
};
for (const entry of discovery.entries) {
if (!shouldLoadHook(entry)) continue;
try {
const hookBaseDir = safeRealpathSync(entry.hook.baseDir);
if (!hookBaseDir) throw new Error(`Hook base directory is no longer readable: ${entry.hook.baseDir}`);
const opened = await openRootFile({
absolutePath: entry.hook.handlerPath,
rootPath: hookBaseDir,
boundaryLabel: "hook directory"
});
if (!opened.ok) throw new Error(`Handler path fails boundary checks: ${entry.hook.handlerPath}`);
const safeHandlerPath = opened.path;
fs.closeSync(opened.fd);
if (entry.hook.source === "openclaw-workspace" || entry.hook.source === "openclaw-managed") log.warn(`Loading ${entry.hook.source.slice(9)} hook code into the gateway process. Hooks are trusted local code.`);
const mod = await import(buildImportUrl(safeHandlerPath, entry.hook.source));
const exportName = entry.metadata?.export ?? "default";
const handler = resolveFunctionModuleExport({
mod,
exportName
});
if (!handler) throw new Error(`Handler '${exportName}' is not a function`);
const events = entry.metadata?.events ?? [];
if (events.length === 0) throw new Error("Hook has no events defined in metadata");
const unknownEvents = events.filter((event) => !isKnownInternalHookEventKey(event));
if (unknownEvents.length > 0) log.warn(`Hook '${sanitizeForLog(entry.hook.name)}' subscribes to event${unknownEvents.length === 1 ? "" : "s"} ${unknownEvents.map((event) => sanitizeForLog(event)).join(", ")} not emitted by OpenClaw core — likely a typo; unless a plugin emits it, the hook never fires. Known events: https://docs.openclaw.ai/automation/hooks`);
registrations.push(...events.map((event) => ({
event,
handler
})));
loadedCount++;
log.debug(`Prepared hook: ${sanitizeForLog(entry.hook.name)} -> ${events.map((event) => sanitizeForLog(event)).join(", ")}${exportName !== "default" ? ` (export: ${sanitizeForLog(exportName)})` : ""}`);
} catch (error) {
const message = `Failed to load hook ${sanitizeForLog(entry.hook.name)}: ${sanitizeForLog(formatErrorMessage(error))}`;
if (opts?.failureMode !== "best-effort") throw new Error(message, { cause: error });
log.error(message);
}
}
return {
loadedCount,
commit({ initial = false } = {}) {
if (initial && (previousGeneration !== hookOwner.generation || previousGeneration.committed)) return false;
resetLoadedInternalHooks();
for (const { event, handler } of registrations) registerInternalHook(event, handler);
hookOwner.generation = {
registrations,
discovery: {
sources: discovery.sources,
declaredNames: selection.declaredNames
},
committed: true
};
setInternalHooksEnabled(enabled);
return true;
}
};
}
//#endregion
export { prepareInternalHooks };