from __future__ import annotations from typing import Any from yuxi.utils.logging_config import logger class ExecApprovalRequest: def __init__( self, request_id: str, command: str, room_id: str, requester_id: str, metadata: dict[str, Any] | None = None, ): self.request_id = request_id self.command = command self.room_id = room_id self.requester_id = requester_id self.status = "pending" self.metadata = metadata or {} self.approver_id: str = "" self.result: str = "" def approve(self, approver_id: str) -> None: self.status = "approved" self.approver_id = approver_id def deny(self, approver_id: str) -> None: self.status = "denied" self.approver_id = approver_id def allow_once(self, approver_id: str) -> None: self.status = "approved" self.approver_id = approver_id self.result = "allow-once" @property def is_resolved(self) -> bool: return self.status in ("approved", "denied") _REACTION_SHORTCUTS = { "\u2705": "allow-once", "\u274c": "deny", "\u2714\ufe0f": "allow-once", "\u2714": "allow-once", "\u274e": "deny", } class ExecApprovalManager: def __init__(self, config: dict[str, Any]): self._config = config approvals_cfg = config.get("execApprovals", {}) enabled_raw = approvals_cfg.get("enabled", "auto") self._enabled = enabled_raw != "false" and enabled_raw is not False self._approvers: list[str] = approvals_cfg.get("approvers", []) self._target = approvals_cfg.get("target", "dm") self._agent_filter: list[str] = approvals_cfg.get("agentFilter", []) self._session_filter: list[str] = approvals_cfg.get("sessionFilter", []) self._pending: dict[str, ExecApprovalRequest] = {} def requires_approval(self, room_id: str, command: str, session_id: str = "") -> bool: if not self._enabled: return False if self._agent_filter and session_id not in self._agent_filter: return False return True def is_approver(self, user_id: str) -> bool: if not self._approvers: return True return user_id in self._approvers def create_request( self, request_id: str, command: str, room_id: str, requester_id: str, ) -> ExecApprovalRequest: request = ExecApprovalRequest( request_id=request_id, command=command, room_id=room_id, requester_id=requester_id, ) self._pending[request_id] = request logger.info(f"Matrix exec approval requested: {request_id} ({command})") return request def handle_reaction(self, user_id: str, emoji: str, request_id: str) -> str | None: if not self.is_approver(user_id): return None action = _REACTION_SHORTCUTS.get(emoji) if not action: return None request = self._pending.get(request_id) if not request: return None if action == "allow-once": request.allow_once(user_id) logger.info(f"Matrix exec approved (once) by {user_id}: {request_id}") elif action == "deny": request.deny(user_id) logger.info(f"Matrix exec denied by {user_id}: {request_id}") return action def approve_request(self, user_id: str, request_id: str) -> bool: if not self.is_approver(user_id): return False request = self._pending.get(request_id) if not request: return False request.approve(user_id) return True def deny_request(self, user_id: str, request_id: str) -> bool: if not self.is_approver(user_id): return False request = self._pending.get(request_id) if not request: return False request.deny(user_id) return True def get_pending(self) -> list[ExecApprovalRequest]: return [r for r in self._pending.values() if r.status == "pending"] def clear_resolved(self) -> None: resolved = [k for k, v in self._pending.items() if v.is_resolved] for key in resolved: del self._pending[key] @property def enabled(self) -> bool: return self._enabled @property def pending_count(self) -> int: return len(self.get_pending())