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:
parent
320b45c051
commit
6e7bfa9633
@ -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);
|
||||||
|
|||||||
91
src/agents/pi-tools.before-call-hook.ts
Normal file
91
src/agents/pi-tools.before-call-hook.ts
Normal 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));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user