feat(plugins): wire up before_tool_call hook for pre-execution validation

Adds the missing wiring for the `before_tool_call` plugin hook, enabling
plugins to intercept, validate, modify, or block tool calls before execution.

The hook infrastructure (types, runner, handler merging) already existed
but was never invoked in the tool execution path. This change:

- Adds `pi-tools.before-call-hook.ts` with tool wrapper functions
- Wraps tools in `attempt.ts` after sanitization if hooks are registered
- Supports three hook actions: allow (default), modify params, or block

Closes #1733

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
fuushyn 2026-01-27 01:39:02 +05:30
parent 320b45c051
commit 6e7bfa9633
2 changed files with 104 additions and 2 deletions

View File

@ -39,6 +39,7 @@ import {
resolveCompactionReserveTokensFloor, resolveCompactionReserveTokensFloor,
} from "../../pi-settings.js"; } from "../../pi-settings.js";
import { createClawdbotCodingTools } from "../../pi-tools.js"; import { createClawdbotCodingTools } from "../../pi-tools.js";
import { wrapToolsWithBeforeCallHook } from "../../pi-tools.before-call-hook.js";
import { resolveSandboxContext } from "../../sandbox.js"; import { resolveSandboxContext } from "../../sandbox.js";
import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js"; import { guardSessionManager } from "../../session-tool-result-guard-wrapper.js";
import { resolveTranscriptPolicy } from "../../transcript-policy.js"; import { resolveTranscriptPolicy } from "../../transcript-policy.js";
@ -229,8 +230,18 @@ export async function runEmbeddedAttempt(
hasRepliedRef: params.hasRepliedRef, hasRepliedRef: params.hasRepliedRef,
modelHasVision, modelHasVision,
}); });
const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider }); const toolsSanitized = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
logToolSchemasForGoogle({ tools, provider: params.provider }); logToolSchemasForGoogle({ tools: toolsSanitized, provider: params.provider });
// Wrap tools with before_tool_call hook if plugins have registered handlers
const hookRunner = getGlobalHookRunner();
const tools = hookRunner?.hasHooks("before_tool_call")
? wrapToolsWithBeforeCallHook(toolsSanitized, {
hookRunner,
agentId: params.agentAccountId,
sessionKey: params.sessionKey ?? params.sessionId,
})
: toolsSanitized;
const machineName = await getMachineDisplayName(); const machineName = await getMachineDisplayName();
const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider); const runtimeChannel = normalizeMessageChannel(params.messageChannel ?? params.messageProvider);

View File

@ -0,0 +1,91 @@
/**
* Tool wrapper that invokes the before_tool_call plugin hook.
*
* This wrapper intercepts tool execution to allow plugins to:
* - Validate and audit tool calls before execution
* - Modify tool parameters
* - Block tool execution with a custom reason
*/
import type { HookRunner } from "../plugins/hooks.js";
import type { PluginHookToolContext } from "../plugins/hooks.js";
import type { AnyAgentTool } from "./pi-tools.types.js";
export interface BeforeCallHookContext {
hookRunner: HookRunner;
agentId?: string;
sessionKey?: string;
}
/**
* Wraps a tool to invoke the before_tool_call hook before execution.
*
* If the hook returns `block: true`, the tool execution is skipped and
* an error message is returned instead.
*
* If the hook returns modified `params`, those are passed to the actual
* tool execute function.
*/
export function wrapToolWithBeforeCallHook(
tool: AnyAgentTool,
ctx: BeforeCallHookContext,
): AnyAgentTool {
const { hookRunner, agentId, sessionKey } = ctx;
const execute = tool.execute;
// If no execute function or no hooks registered, return tool unchanged
if (!execute || !hookRunner.hasHooks("before_tool_call")) {
return tool;
}
return {
...tool,
execute: async (toolCallId, params, signal, onUpdate) => {
const toolName = tool.name;
// Build hook context
const hookCtx: PluginHookToolContext = {
agentId,
sessionKey,
toolName,
toolCallId,
};
// Run the before_tool_call hook
const hookResult = await hookRunner.runBeforeToolCall(
{
toolName,
toolCallId,
params: params as Record<string, unknown>,
},
hookCtx,
);
// Check if the hook blocked the tool call
if (hookResult?.block) {
const reason = hookResult.blockReason ?? "Tool call blocked by policy";
// Return a structured error that the agent can understand
return {
type: "text" as const,
text: `Error: ${reason}`,
};
}
// Use modified params if provided, otherwise use original
const effectiveParams = hookResult?.params ?? params;
// Execute the actual tool
return await execute(toolCallId, effectiveParams, signal, onUpdate);
},
};
}
/**
* Wraps all tools in the array with the before_tool_call hook.
*/
export function wrapToolsWithBeforeCallHook(
tools: AnyAgentTool[],
ctx: BeforeCallHookContext,
): AnyAgentTool[] {
return tools.map((tool) => wrapToolWithBeforeCallHook(tool, ctx));
}