"""Permission gating for tool execution.""" from __future__ import annotations import logging from collections.abc import Awaitable, Callable from app.models.config import PermissionsConfig logger = logging.getLogger(__name__) # Type alias for the async prompt callback PromptCallback = Callable[[str, str], Awaitable[bool]] class PermissionDenied(Exception): """Raised when a tool is denied execution by permissions policy.""" class PermissionsService: """Check whether a tool is allowed to execute based on config tiers. In TUI mode, set a prompt callback via set_prompt_callback() that shows a modal dialog. Without a callback, unlisted tools are denied. """ def __init__(self, config: PermissionsConfig) -> None: self.config = config self._prompt_callback: PromptCallback | None = None def set_prompt_callback(self, callback: PromptCallback) -> None: """Set the async callback used to prompt the user for permission. Args: callback: Async function(tool_name, description) -> bool. """ self._prompt_callback = callback async def check(self, tool_name: str, description: str = "") -> bool: """Check if a tool is permitted to run. Returns: True if permitted, False if denied. """ if tool_name in self.config.deny: logger.info("Tool '%s' is in deny list — blocked", tool_name) return False if tool_name in self.config.auto_approve: logger.debug("Tool '%s' is auto-approved", tool_name) return True # Prompt user via callback (TUI modal, etc.) if self._prompt_callback is not None: return await self._prompt_callback(tool_name, description) # No callback set — deny by default (safe fallback) logger.warning("Tool '%s' requires approval but no prompt callback set — denied", tool_name) return False