150 lines
4.4 KiB
Python
150 lines
4.4 KiB
Python
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())
|