Source code for jeevesagent.security.hooks
"""User-registered lifecycle callbacks.
Hooks run in a timeout-shielded scope so a buggy callback can't hang
the loop. Pre-tool hooks can deny a call (first deny wins); post-tool
hooks are best-effort and can never affect the result.
"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
import anyio
from ..core.types import Event, PermissionDecision, ToolCall, ToolResult
PreToolHook = Callable[[ToolCall], Awaitable[PermissionDecision | None]]
PostToolHook = Callable[[ToolCall, ToolResult], Awaitable[None]]
EventHook = Callable[[Event], Awaitable[None]]
[docs]
@dataclass
class HookRegistry:
"""Implements :class:`~jeevesagent.core.protocols.HookHost`."""
pre_tool_hooks: list[PreToolHook] = field(default_factory=list)
post_tool_hooks: list[PostToolHook] = field(default_factory=list)
event_hooks: list[EventHook] = field(default_factory=list)
hook_timeout_s: float = 5.0
# ---- registration ----------------------------------------------------
[docs]
def register_post_tool(self, hook: PostToolHook) -> PostToolHook:
self.post_tool_hooks.append(hook)
return hook
[docs]
def register_event(self, hook: EventHook) -> EventHook:
self.event_hooks.append(hook)
return hook
# ---- HookHost protocol ----------------------------------------------
[docs]
async def post_tool(self, call: ToolCall, result: ToolResult) -> None:
"""Best-effort post-tool callbacks. Failures and timeouts are
absorbed so they cannot affect the result the loop returns."""
for hook in self.post_tool_hooks:
with anyio.move_on_after(self.hook_timeout_s):
try:
await hook(call, result)
except Exception: # noqa: BLE001 — hooks must never break the loop
continue
[docs]
async def on_event(self, event: Event) -> None:
for hook in self.event_hooks:
with anyio.move_on_after(self.hook_timeout_s):
try:
await hook(event)
except Exception: # noqa: BLE001
continue