Files
SneakyCode/app/services/permissions.py

60 lines
1.9 KiB
Python

"""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