2026-05-12 00:48:25 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-12 14:51:53 +08:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
2026-05-12 00:48:25 +08:00
|
|
|
from enum import StrEnum
|
|
|
|
|
from typing import Any
|
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
|
|
2026-05-12 14:51:53 +08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
EXEC_AUTH_TIMEOUT = 30.0
|
|
|
|
|
|
2026-05-12 00:48:25 +08:00
|
|
|
|
|
|
|
|
class ExecAuthResult(StrEnum):
|
|
|
|
|
APPROVED = "approved"
|
|
|
|
|
DENIED = "denied"
|
|
|
|
|
PENDING = "pending"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecAuthAdapter:
|
|
|
|
|
def __init__(self, auto_approve: bool = False):
|
|
|
|
|
self._auto_approve = auto_approve
|
|
|
|
|
self._pending_requests: dict[str, dict] = {}
|
2026-05-12 14:51:53 +08:00
|
|
|
self._request_events: dict[str, asyncio.Event] = {}
|
2026-05-12 00:48:25 +08:00
|
|
|
self._request_counter = 0
|
|
|
|
|
self._on_approval_request: Callable[[dict], Awaitable[None]] | None = None
|
|
|
|
|
|
2026-05-12 14:51:53 +08:00
|
|
|
@property
|
|
|
|
|
def auto_approve(self) -> bool:
|
|
|
|
|
return self._auto_approve
|
|
|
|
|
|
2026-05-12 00:48:25 +08:00
|
|
|
def on_approval_request(self, handler: Callable[[dict], Awaitable[None]]) -> None:
|
|
|
|
|
self._on_approval_request = handler
|
|
|
|
|
|
2026-05-12 14:51:53 +08:00
|
|
|
async def request_approval(
|
|
|
|
|
self, action: str, params: dict[str, Any], user_id: str, timeout: float = EXEC_AUTH_TIMEOUT
|
|
|
|
|
) -> ExecAuthResult:
|
2026-05-12 00:48:25 +08:00
|
|
|
if self._auto_approve:
|
|
|
|
|
return ExecAuthResult.APPROVED
|
|
|
|
|
|
|
|
|
|
self._request_counter += 1
|
|
|
|
|
request_id = f"exec_auth_{self._request_counter}"
|
2026-05-12 14:51:53 +08:00
|
|
|
event = asyncio.Event()
|
2026-05-12 00:48:25 +08:00
|
|
|
request = {
|
|
|
|
|
"id": request_id,
|
|
|
|
|
"action": action,
|
|
|
|
|
"params": params,
|
|
|
|
|
"user_id": user_id,
|
|
|
|
|
"result": ExecAuthResult.PENDING,
|
|
|
|
|
}
|
|
|
|
|
self._pending_requests[request_id] = request
|
2026-05-12 14:51:53 +08:00
|
|
|
self._request_events[request_id] = event
|
2026-05-12 00:48:25 +08:00
|
|
|
|
|
|
|
|
if self._on_approval_request:
|
|
|
|
|
await self._on_approval_request(request)
|
|
|
|
|
|
2026-05-12 14:51:53 +08:00
|
|
|
try:
|
|
|
|
|
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
|
|
|
except TimeoutError:
|
|
|
|
|
logger.warning(f"Exec auth request {request_id} timed out after {timeout}s")
|
|
|
|
|
request["result"] = ExecAuthResult.DENIED
|
|
|
|
|
self._cleanup_request(request_id)
|
|
|
|
|
return ExecAuthResult.DENIED
|
|
|
|
|
|
|
|
|
|
result = request.get("result", ExecAuthResult.DENIED)
|
|
|
|
|
self._cleanup_request(request_id)
|
|
|
|
|
return ExecAuthResult(result)
|
2026-05-12 00:48:25 +08:00
|
|
|
|
|
|
|
|
def approve(self, request_id: str) -> ExecAuthResult:
|
|
|
|
|
request = self._pending_requests.get(request_id)
|
|
|
|
|
if not request:
|
|
|
|
|
return ExecAuthResult.DENIED
|
|
|
|
|
request["result"] = ExecAuthResult.APPROVED
|
2026-05-12 14:51:53 +08:00
|
|
|
event = self._request_events.get(request_id)
|
|
|
|
|
if event:
|
|
|
|
|
event.set()
|
2026-05-12 00:48:25 +08:00
|
|
|
return ExecAuthResult.APPROVED
|
|
|
|
|
|
|
|
|
|
def deny(self, request_id: str) -> ExecAuthResult:
|
|
|
|
|
request = self._pending_requests.get(request_id)
|
|
|
|
|
if not request:
|
|
|
|
|
return ExecAuthResult.DENIED
|
|
|
|
|
request["result"] = ExecAuthResult.DENIED
|
2026-05-12 14:51:53 +08:00
|
|
|
event = self._request_events.get(request_id)
|
|
|
|
|
if event:
|
|
|
|
|
event.set()
|
2026-05-12 00:48:25 +08:00
|
|
|
return ExecAuthResult.DENIED
|
|
|
|
|
|
|
|
|
|
def get_pending(self) -> list[dict]:
|
|
|
|
|
return [r for r in self._pending_requests.values() if r["result"] == ExecAuthResult.PENDING]
|
2026-05-12 14:51:53 +08:00
|
|
|
|
|
|
|
|
def _cleanup_request(self, request_id: str) -> None:
|
|
|
|
|
self._pending_requests.pop(request_id, None)
|
|
|
|
|
self._request_events.pop(request_id, None)
|