mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:40:42 +00:00
Summary: - The PR adds a `before_agent_run` plugin hook with pass/block decisions, redacted blocked-turn persistence, diagnostics/docs/changelog updates, and focused runner, gateway, session, and plugin tests. - Reproducibility: not applicable. as a feature PR rather than a current-main bug report. Current main lacks ` ... un`, while the PR head adds source coverage and copied live Gateway/WebChat log proof for the new behavior. Automerge notes: - PR branch already contained follow-up commit before automerge: fix: trim before agent hook PR scope - PR branch already contained follow-up commit before automerge: fix: keep before-agent blocks redacted - PR branch already contained follow-up commit before automerge: fix: keep runtime context out of model prompt - PR branch already contained follow-up commit before automerge: docs: refresh config baseline after rebase - PR branch already contained follow-up commit before automerge: fix: align blocked turn clients with redacted content - PR branch already contained follow-up commit before automerge: fix: remove out-of-scope client block UI changes Validation: - ClawSweeper review passed for head767e46fde8. - Required merge gates passed before the squash merge. Prepared head SHA:767e46fde8Review: https://github.com/openclaw/openclaw/pull/75035#issuecomment-4351843275 Co-authored-by: Jesse Merhi <jessejmerhi@gmail.com> Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
/**
|
|
* Global Plugin Hook Runner
|
|
*
|
|
* Singleton hook runner that's initialized when plugins are loaded
|
|
* and can be called from anywhere in the codebase.
|
|
*/
|
|
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
|
import type { GlobalHookRunnerRegistry } from "./hook-registry.types.js";
|
|
import type { PluginHookGatewayContext, PluginHookGatewayStopEvent } from "./hook-types.js";
|
|
import { createHookRunner, type HookRunner } from "./hooks.js";
|
|
|
|
type HookRunnerGlobalState = {
|
|
hookRunner: HookRunner | null;
|
|
registry: GlobalHookRunnerRegistry | null;
|
|
};
|
|
|
|
const hookRunnerGlobalStateKey = Symbol.for("openclaw.plugins.hook-runner-global-state");
|
|
const getState = () =>
|
|
resolveGlobalSingleton<HookRunnerGlobalState>(hookRunnerGlobalStateKey, () => ({
|
|
hookRunner: null,
|
|
registry: null,
|
|
}));
|
|
|
|
const getLog = () => createSubsystemLogger("plugins");
|
|
|
|
/**
|
|
* Initialize the global hook runner with a plugin registry.
|
|
* Called once when plugins are loaded during gateway startup.
|
|
*/
|
|
export function initializeGlobalHookRunner(registry: GlobalHookRunnerRegistry): void {
|
|
const state = getState();
|
|
const log = getLog();
|
|
state.registry = registry;
|
|
state.hookRunner = createHookRunner(registry, {
|
|
logger: {
|
|
debug: (msg) => log.debug(msg),
|
|
warn: (msg) => log.warn(msg),
|
|
error: (msg) => log.error(msg),
|
|
},
|
|
catchErrors: true,
|
|
failurePolicyByHook: {
|
|
before_agent_run: "fail-closed",
|
|
before_tool_call: "fail-closed",
|
|
},
|
|
});
|
|
|
|
const hookCount = registry.hooks.length;
|
|
if (hookCount > 0) {
|
|
log.debug(`hook runner initialized with ${hookCount} registered hooks`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the global hook runner.
|
|
* Returns null if plugins haven't been loaded yet.
|
|
*/
|
|
export function getGlobalHookRunner(): HookRunner | null {
|
|
return getState().hookRunner;
|
|
}
|
|
|
|
/**
|
|
* Get the global plugin registry.
|
|
* Returns null if plugins haven't been loaded yet.
|
|
*/
|
|
export function getGlobalPluginRegistry(): GlobalHookRunnerRegistry | null {
|
|
return getState().registry;
|
|
}
|
|
|
|
/**
|
|
* Check if any hooks are registered for a given hook name.
|
|
*/
|
|
export function hasGlobalHooks(hookName: Parameters<HookRunner["hasHooks"]>[0]): boolean {
|
|
return getState().hookRunner?.hasHooks(hookName) ?? false;
|
|
}
|
|
|
|
export async function runGlobalGatewayStopSafely(params: {
|
|
event: PluginHookGatewayStopEvent;
|
|
ctx: PluginHookGatewayContext;
|
|
onError?: (err: unknown) => void;
|
|
}): Promise<void> {
|
|
const log = getLog();
|
|
const hookRunner = getGlobalHookRunner();
|
|
if (!hookRunner?.hasHooks("gateway_stop")) {
|
|
return;
|
|
}
|
|
try {
|
|
await hookRunner.runGatewayStop(params.event, params.ctx);
|
|
} catch (err) {
|
|
if (params.onError) {
|
|
params.onError(err);
|
|
return;
|
|
}
|
|
log.warn(`gateway_stop hook failed: ${String(err)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the global hook runner (for testing).
|
|
*/
|
|
export function resetGlobalHookRunner(): void {
|
|
const state = getState();
|
|
state.hookRunner = null;
|
|
state.registry = null;
|
|
}
|