Merge 5dc2c6e0f50216980cde6bf15ee1e527ffc84667 into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
e94e15401d
@ -213,6 +213,31 @@ export async function dispatchReplyFromConfig(params: {
|
||||
wasMentioned: typeof ctx.WasMentioned === "boolean" ? ctx.WasMentioned : undefined,
|
||||
});
|
||||
|
||||
// Run before_dispatch plugin hooks (blocking — can abort dispatch before LLM).
|
||||
// This lets security plugins (e.g. authentication gates) prevent LLM invocation
|
||||
// entirely for unauthenticated sessions, saving tokens and enforcing access.
|
||||
if (hookRunner?.hasHooks("before_dispatch") && sessionKey) {
|
||||
const beforeDispatchResult = await hookRunner.runBeforeDispatch(
|
||||
{
|
||||
sessionKey,
|
||||
channelId: channel,
|
||||
senderId: ctx.SenderId,
|
||||
conversationId: hookContext.conversationId,
|
||||
isGroup,
|
||||
content: hookContext.content,
|
||||
messageId: messageIdForHook,
|
||||
},
|
||||
toPluginMessageContext(hookContext),
|
||||
);
|
||||
if (beforeDispatchResult?.block) {
|
||||
const queuedFinal = beforeDispatchResult.replyText
|
||||
? dispatcher.sendFinalReply({ text: beforeDispatchResult.replyText })
|
||||
: false;
|
||||
recordProcessed("skipped", { reason: "before_dispatch_blocked" });
|
||||
return { queuedFinal, counts: dispatcher.getQueuedCounts() };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should route replies to originating channel instead of dispatcher.
|
||||
// Only route when the originating channel is DIFFERENT from the current surface.
|
||||
// This handles cross-provider routing (e.g., message from Telegram being processed
|
||||
|
||||
@ -53,6 +53,8 @@ import type {
|
||||
PluginHookToolResultPersistResult,
|
||||
PluginHookBeforeMessageWriteEvent,
|
||||
PluginHookBeforeMessageWriteResult,
|
||||
PluginHookBeforeDispatchEvent,
|
||||
PluginHookBeforeDispatchResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Re-export types for consumers
|
||||
@ -87,6 +89,8 @@ export type {
|
||||
PluginHookToolResultPersistResult,
|
||||
PluginHookBeforeMessageWriteEvent,
|
||||
PluginHookBeforeMessageWriteResult,
|
||||
PluginHookBeforeDispatchEvent,
|
||||
PluginHookBeforeDispatchResult,
|
||||
PluginHookSessionContext,
|
||||
PluginHookSessionStartEvent,
|
||||
PluginHookSessionEndEvent,
|
||||
@ -622,6 +626,55 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
||||
return runVoidHook("message_sent", event, ctx);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Dispatch Hooks
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Run before_dispatch hook.
|
||||
* Allows plugins to block the entire message dispatch before LLM invocation
|
||||
* and optionally send a reply text directly to the user.
|
||||
* Runs sequentially — first handler that returns { block: true } wins.
|
||||
*
|
||||
* **Error semantics:** Like all hooks, errors are caught and logged when
|
||||
* `catchErrors` is true (the default). This means a throwing handler
|
||||
* results in permit-by-default (fail-open). Security-critical plugins
|
||||
* should handle errors internally to implement fail-closed behavior.
|
||||
*/
|
||||
async function runBeforeDispatch(
|
||||
event: PluginHookBeforeDispatchEvent,
|
||||
ctx: PluginHookMessageContext,
|
||||
): Promise<PluginHookBeforeDispatchResult | undefined> {
|
||||
const hooks = getHooksForName(registry, "before_dispatch");
|
||||
if (hooks.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
logger?.debug?.(`[hooks] running before_dispatch (${hooks.length} handlers, sequential)`);
|
||||
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
const result = await (
|
||||
hook.handler as (
|
||||
event: PluginHookBeforeDispatchEvent,
|
||||
ctx: PluginHookMessageContext,
|
||||
) =>
|
||||
| Promise<PluginHookBeforeDispatchResult | void>
|
||||
| PluginHookBeforeDispatchResult
|
||||
| void
|
||||
)(event, ctx);
|
||||
|
||||
if (result?.block) {
|
||||
return result;
|
||||
}
|
||||
} catch (err) {
|
||||
handleHookError({ hookName: "before_dispatch", pluginId: hook.pluginId, error: err });
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tool Hooks
|
||||
// =========================================================================
|
||||
@ -936,6 +989,8 @@ export function createHookRunner(registry: PluginRegistry, options: HookRunnerOp
|
||||
runMessageReceived,
|
||||
runMessageSending,
|
||||
runMessageSent,
|
||||
// Dispatch hooks
|
||||
runBeforeDispatch,
|
||||
// Tool hooks
|
||||
runBeforeToolCall,
|
||||
runAfterToolCall,
|
||||
|
||||
@ -1391,6 +1391,7 @@ export type PluginHookName =
|
||||
| "subagent_delivery_target"
|
||||
| "subagent_spawned"
|
||||
| "subagent_ended"
|
||||
| "before_dispatch"
|
||||
| "gateway_start"
|
||||
| "gateway_stop";
|
||||
|
||||
@ -1418,6 +1419,7 @@ export const PLUGIN_HOOK_NAMES = [
|
||||
"subagent_delivery_target",
|
||||
"subagent_spawned",
|
||||
"subagent_ended",
|
||||
"before_dispatch",
|
||||
"gateway_start",
|
||||
"gateway_stop",
|
||||
] as const satisfies readonly PluginHookName[];
|
||||
@ -1750,6 +1752,26 @@ export type PluginHookBeforeMessageWriteResult = {
|
||||
message?: AgentMessage; // Optional: modified message to write instead
|
||||
};
|
||||
|
||||
// before_dispatch hook — fired in dispatchReplyFromConfig before LLM invocation.
|
||||
// Allows plugins to block the entire dispatch (preventing LLM processing) and
|
||||
// optionally send a direct reply to the user.
|
||||
export type PluginHookBeforeDispatchEvent = {
|
||||
sessionKey: string;
|
||||
channelId: string;
|
||||
senderId?: string;
|
||||
conversationId?: string;
|
||||
isGroup: boolean;
|
||||
content: string;
|
||||
messageId?: string;
|
||||
};
|
||||
|
||||
export type PluginHookBeforeDispatchResult = {
|
||||
/** If true, the dispatch is aborted — no LLM invocation occurs. */
|
||||
block?: boolean;
|
||||
/** If block is true, this text is sent as a direct reply to the user. */
|
||||
replyText?: string;
|
||||
};
|
||||
|
||||
// Session context
|
||||
export type PluginHookSessionContext = {
|
||||
agentId?: string;
|
||||
@ -1959,6 +1981,10 @@ export type PluginHookHandlerMap = {
|
||||
event: PluginHookSubagentEndedEvent,
|
||||
ctx: PluginHookSubagentContext,
|
||||
) => Promise<void> | void;
|
||||
before_dispatch: (
|
||||
event: PluginHookBeforeDispatchEvent,
|
||||
ctx: PluginHookMessageContext,
|
||||
) => Promise<PluginHookBeforeDispatchResult | void> | PluginHookBeforeDispatchResult | void;
|
||||
gateway_start: (
|
||||
event: PluginHookGatewayStartEvent,
|
||||
ctx: PluginHookGatewayContext,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user