← Back to PRs

#18810: feat(hooks): wire before_tool_call/after_tool_call with veto support

by vjranagit open 2026-02-17 03:04 View on GitHub →
docs channel: mattermost agents size: L
# PR: Wire before_tool_call and after_tool_call Hooks with Veto Support ## Summary This PR implements real-time tool call interception and veto capability by wiring the existing `before_tool_call` and `after_tool_call` hooks into the actual tool execution pipeline. ## What Changed ### Hook Wiring - **`src/agents/pi-tool-definition-adapter.ts`**: Added `hookContext` parameter to `toToolDefinitions()` to pass `agentId` and `sessionKey` to hooks. Fixed `runAfterToolCall` calls to include full context. - **`src/agents/pi-tools.before-tool-call.ts`**: Added `after_tool_call` emission in `wrapToolWithBeforeToolCallHook` for all outcomes (success, error, blocked). - **`src/plugins/types.ts`**: Added `blocked` and `blockReason` fields to `PluginHookAfterToolCallEvent`. ### Tests - **`src/plugins/hooks.tool-call.test.ts`**: New test file with 9 test cases covering hook firing, veto mechanism, payload structure, and context propagation. ## Why It Matters Before this change: - `before_tool_call` and `after_tool_call` were defined but NOT wired into tool execution - Plugins could not block tool execution in real-time - `after_tool_call` received incomplete context (missing `agentId`, `sessionKey`) After this change: - Plugins can intercept tool calls BEFORE execution - Returning `{ block: true, blockReason: "..." }` prevents tool execution - `after_tool_call` includes blocked status and full context - sessionKey is consistently available for session tracking ## Veto Contract ```typescript // In before_tool_call handler: api.on("before_tool_call", async (event, ctx) => { if (shouldBlock(event.toolName, event.params)) { return { block: true, blockReason: "Policy violation" }; } return undefined; // allow }); ``` When `block: true` is returned: 1. Tool execution does NOT run 2. Error is thrown with `blockReason` 3. `after_tool_call` fires with `blocked: true` ## Event Payload Schema ### before_tool_call ```typescript event: { toolName: string; params: Record<string, unknown> } ctx: { agentId?: string; sessionKey?: string; toolName: string } result: { params?: Record<string, unknown>; block?: boolean; blockReason?: string } ``` ### after_tool_call ```typescript event: { toolName: string; params: Record<string, unknown>; result?: unknown; error?: string; durationMs?: number; blocked?: boolean; // NEW blockReason?: string; // NEW } ctx: { agentId?: string; sessionKey?: string; toolName: string } ``` ## Backward Compatibility - All new fields are optional - Default behavior (no hooks registered) unchanged - Existing plugins continue to work without modification ## Security Impact - Plugins can now block dangerous tool calls in real-time - Fail-closed: if hook throws, tool execution continues (resilient) - Policy enforcement happens BEFORE side effects ## Testing ```bash pnpm test src/plugins/hooks.tool-call.test.ts ``` 9 test cases covering: 1. before_tool_call receives correct payload 2. Veto blocks tool execution 3. Blocked call includes blockReason 4. after_tool_call receives result/error/durationMs 5. sessionKey present in context 6. Parameter modification works 7. No-hooks bypass works 8. Hook error resilience 9. Error case handling ## Files Changed | File | Changes | |------|---------| | `src/plugins/types.ts` | +2 lines (blocked, blockReason fields) | | `src/agents/pi-tool-definition-adapter.ts` | ~50 lines (context wiring) | | `src/agents/pi-tools.before-tool-call.ts` | ~30 lines (after_tool_call emission) | | `src/plugins/hooks.tool-call.test.ts` | NEW (9 test cases) | ## Related - docs/hooks_tool_calls.md - Hook documentation (created in this branch) - tasks-hooks/ - Implementation task files

Most Similar PRs