mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 19:32:27 +00:00
144 lines
4.2 KiB
TypeScript
144 lines
4.2 KiB
TypeScript
import type { OpenClawConfig, HookConfig } from "../config/config.js";
|
|
import { resolveHookKey } from "./frontmatter.js";
|
|
import type { HookEntry, HookSource } from "./types.js";
|
|
|
|
export type HookEnableStateReason = "disabled in config" | "workspace hook (disabled by default)";
|
|
|
|
export type HookEnableState = {
|
|
enabled: boolean;
|
|
reason?: HookEnableStateReason;
|
|
};
|
|
|
|
export type HookSourcePolicy = {
|
|
precedence: number;
|
|
trustedLocalCode: boolean;
|
|
defaultEnableMode: "default-on" | "explicit-opt-in";
|
|
canOverride: HookSource[];
|
|
canBeOverriddenBy: HookSource[];
|
|
};
|
|
|
|
export type HookResolutionCollision = {
|
|
name: string;
|
|
kept: HookEntry;
|
|
ignored: HookEntry;
|
|
};
|
|
|
|
const HOOK_SOURCE_POLICIES: Record<HookSource, HookSourcePolicy> = {
|
|
"openclaw-bundled": {
|
|
precedence: 10,
|
|
trustedLocalCode: true,
|
|
defaultEnableMode: "default-on",
|
|
canOverride: ["openclaw-bundled"],
|
|
canBeOverriddenBy: ["openclaw-managed", "openclaw-plugin"],
|
|
},
|
|
"openclaw-plugin": {
|
|
precedence: 20,
|
|
trustedLocalCode: true,
|
|
defaultEnableMode: "default-on",
|
|
canOverride: ["openclaw-bundled", "openclaw-plugin"],
|
|
canBeOverriddenBy: ["openclaw-managed"],
|
|
},
|
|
"openclaw-managed": {
|
|
precedence: 30,
|
|
trustedLocalCode: true,
|
|
defaultEnableMode: "default-on",
|
|
canOverride: ["openclaw-bundled", "openclaw-managed", "openclaw-plugin"],
|
|
canBeOverriddenBy: ["openclaw-managed"],
|
|
},
|
|
"openclaw-workspace": {
|
|
precedence: 40,
|
|
trustedLocalCode: true,
|
|
defaultEnableMode: "explicit-opt-in",
|
|
canOverride: ["openclaw-workspace"],
|
|
canBeOverriddenBy: ["openclaw-workspace"],
|
|
},
|
|
};
|
|
|
|
export function getHookSourcePolicy(source: HookSource): HookSourcePolicy {
|
|
return HOOK_SOURCE_POLICIES[source];
|
|
}
|
|
|
|
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<string, HookConfig | undefined>)[hookKey];
|
|
if (!entry || typeof entry !== "object") {
|
|
return undefined;
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
export function resolveHookEnableState(params: {
|
|
entry: HookEntry;
|
|
config?: OpenClawConfig;
|
|
hookConfig?: HookConfig;
|
|
}): HookEnableState {
|
|
const { entry, config } = params;
|
|
const hookKey = resolveHookKey(entry.hook.name, entry);
|
|
const hookConfig = params.hookConfig ?? resolveHookConfig(config, hookKey);
|
|
|
|
if (entry.hook.source === "openclaw-plugin") {
|
|
return { enabled: true };
|
|
}
|
|
if (hookConfig?.enabled === false) {
|
|
return { enabled: false, reason: "disabled in config" };
|
|
}
|
|
|
|
const sourcePolicy = getHookSourcePolicy(entry.hook.source);
|
|
if (sourcePolicy.defaultEnableMode === "explicit-opt-in" && hookConfig?.enabled !== true) {
|
|
return { enabled: false, reason: "workspace hook (disabled by default)" };
|
|
}
|
|
|
|
return { enabled: true };
|
|
}
|
|
|
|
function canOverrideHook(candidate: HookEntry, existing: HookEntry): boolean {
|
|
const candidatePolicy = getHookSourcePolicy(candidate.hook.source);
|
|
const existingPolicy = getHookSourcePolicy(existing.hook.source);
|
|
return (
|
|
candidatePolicy.canOverride.includes(existing.hook.source) &&
|
|
existingPolicy.canBeOverriddenBy.includes(candidate.hook.source)
|
|
);
|
|
}
|
|
|
|
export function resolveHookEntries(
|
|
entries: HookEntry[],
|
|
opts?: {
|
|
onCollisionIgnored?: (collision: HookResolutionCollision) => void;
|
|
},
|
|
): HookEntry[] {
|
|
const ordered = entries
|
|
.map((entry, index) => ({ entry, index }))
|
|
.toSorted((a, b) => {
|
|
const precedenceDelta =
|
|
getHookSourcePolicy(a.entry.hook.source).precedence -
|
|
getHookSourcePolicy(b.entry.hook.source).precedence;
|
|
return precedenceDelta !== 0 ? precedenceDelta : a.index - b.index;
|
|
});
|
|
|
|
const merged = new Map<string, HookEntry>();
|
|
for (const { entry } of ordered) {
|
|
const existing = merged.get(entry.hook.name);
|
|
if (!existing) {
|
|
merged.set(entry.hook.name, entry);
|
|
continue;
|
|
}
|
|
if (canOverrideHook(entry, existing)) {
|
|
merged.set(entry.hook.name, entry);
|
|
continue;
|
|
}
|
|
opts?.onCollisionIgnored?.({
|
|
name: entry.hook.name,
|
|
kept: existing,
|
|
ignored: entry,
|
|
});
|
|
}
|
|
|
|
return Array.from(merged.values());
|
|
}
|