Source code for dogwood.integrations.strands.hooks

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from dogwood import native
from dogwood.integrations.strands.common import (
    IdentityResolver,
    InputMapper,
    _authorize_event,
    _is_allowed,
    default_principal,
    default_resource,
    default_tool_input,
)


[docs] @dataclass class StrandsPolicyHook: """Authorize Strands tool calls with a Dogwood native authorizer. Register instances of this class as Strands hooks for ``BeforeToolCallEvent``. If Dogwood denies the request, the hook sets ``event.cancel_tool`` with a denial message, which prevents Strands from invoking the tool. """ authorizer: native.NativeAuthorizer action: str = "Drupe::Action::CallTool" principal: str | IdentityResolver = default_principal resource: str | IdentityResolver = default_resource input_mapper: InputMapper = default_tool_input deny_message: str = "Dogwood policy denied this tool call." def __call__(self, event: Any) -> None: if not _is_allowed(_authorize_event(self, event)): event.cancel_tool = self.deny_message
[docs] def before_tool_call_hook( policy_source: str, policy_schema_source: str, *, event_schema_source: str | None = None, action: str = "Drupe::Action::CallTool", principal: str | IdentityResolver = default_principal, resource: str | IdentityResolver = default_resource, input_mapper: InputMapper = default_tool_input, deny_message: str = "Dogwood policy denied this tool call.", ) -> StrandsPolicyHook: """Create a Strands ``BeforeToolCallEvent`` hook backed by Dogwood. The native Dogwood authorizer is persistent, so policy parsing and lowering happen once when this hook is constructed. """ return _build_policy_hook( policy_source, policy_schema_source, event_schema_source=event_schema_source, action=action, principal=principal, resource=resource, input_mapper=input_mapper, deny_message=deny_message, )
[docs] def attach_before_tool_call_hook(agent: Any, hook: StrandsPolicyHook) -> None: """Attach a Dogwood policy hook to a Strands agent. This imports Strands only when called, keeping ``dogwood-py`` free of a hard Strands dependency. """ try: from strands.hooks import BeforeToolCallEvent except ImportError as exc: # pragma: no cover - depends on optional package raise RuntimeError( "Strands integration requires the optional `strands-agents` package" ) from exc if hasattr(agent, "hooks") and hasattr(agent.hooks, "add_callback"): agent.hooks.add_callback(BeforeToolCallEvent, hook) return if hasattr(agent, "add_hook"): agent.add_hook(hook, BeforeToolCallEvent) return raise TypeError("agent does not expose a supported Strands hook registration API")
def _build_policy_hook( policy_source: str | None, policy_schema_source: str | None, *, event_schema_source: str | None = None, authorizer: native.NativeAuthorizer | None = None, action: str, principal: str | IdentityResolver, resource: str | IdentityResolver, input_mapper: InputMapper, deny_message: str, ) -> StrandsPolicyHook: if authorizer is None: if policy_source is None or policy_schema_source is None: raise TypeError( "policy_source and policy_schema_source are required when " "authorizer is not supplied" ) authorizer = native.NativeAuthorizer( policy_source, policy_schema_source, event_schema_source, ) return StrandsPolicyHook( authorizer=authorizer, action=action, principal=principal, resource=resource, input_mapper=input_mapper, deny_message=deny_message, ) __all__ = [ "StrandsPolicyHook", "attach_before_tool_call_hook", "before_tool_call_hook", ]