Files
kuma-strapper/backend/utils/approval.py
Debian ea49143a13
All checks were successful
Build Container / build (push) Successful in 1m18s
Initial commit with CI workflow
- Flask backend with SSH discovery and Claude AI integration
- React/Vite frontend with Tailwind CSS
- Docker multi-stage build
- Gitea Actions workflow for container builds

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 21:38:50 +00:00

214 lines
6.4 KiB
Python

import uuid
import threading
from datetime import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional, Callable
class ApprovalStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
class ApprovalType(Enum):
SSH_COMMAND = "ssh_command"
CREATE_MONITOR = "create_monitor"
@dataclass
class ApprovalRequest:
"""A request waiting for user approval."""
id: str
type: ApprovalType
description: str
details: dict
status: ApprovalStatus = ApprovalStatus.PENDING
created_at: datetime = field(default_factory=datetime.now)
resolved_at: Optional[datetime] = None
reason: str = "" # Why Claude wants to do this
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
"id": self.id,
"type": self.type.value,
"description": self.description,
"details": self.details,
"status": self.status.value,
"created_at": self.created_at.isoformat(),
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
"reason": self.reason,
}
class ApprovalQueue:
"""Queue for managing approval requests in dev mode."""
def __init__(self):
self._requests: dict[str, ApprovalRequest] = {}
self._lock = threading.Lock()
self._on_request_added: Optional[Callable[[ApprovalRequest], None]] = None
self._on_request_resolved: Optional[Callable[[ApprovalRequest], None]] = None
def set_callbacks(
self,
on_added: Optional[Callable[[ApprovalRequest], None]] = None,
on_resolved: Optional[Callable[[ApprovalRequest], None]] = None,
) -> None:
"""Set callbacks for queue events."""
self._on_request_added = on_added
self._on_request_resolved = on_resolved
def add_ssh_command(self, command: str, reason: str, hostname: str) -> ApprovalRequest:
"""Add an SSH command approval request."""
request = ApprovalRequest(
id=str(uuid.uuid4()),
type=ApprovalType.SSH_COMMAND,
description=f"Execute SSH command on {hostname}",
details={
"command": command,
"hostname": hostname,
},
reason=reason,
)
with self._lock:
self._requests[request.id] = request
if self._on_request_added:
self._on_request_added(request)
return request
def add_monitor_creation(
self,
monitor_name: str,
monitor_type: str,
target: str,
reason: str,
) -> ApprovalRequest:
"""Add a monitor creation approval request."""
request = ApprovalRequest(
id=str(uuid.uuid4()),
type=ApprovalType.CREATE_MONITOR,
description=f"Create {monitor_type} monitor: {monitor_name}",
details={
"name": monitor_name,
"type": monitor_type,
"target": target,
},
reason=reason,
)
with self._lock:
self._requests[request.id] = request
if self._on_request_added:
self._on_request_added(request)
return request
def approve(self, request_id: str) -> Optional[ApprovalRequest]:
"""Approve a request."""
with self._lock:
request = self._requests.get(request_id)
if not request or request.status != ApprovalStatus.PENDING:
return None
request.status = ApprovalStatus.APPROVED
request.resolved_at = datetime.now()
if self._on_request_resolved:
self._on_request_resolved(request)
return request
def reject(self, request_id: str) -> Optional[ApprovalRequest]:
"""Reject a request."""
with self._lock:
request = self._requests.get(request_id)
if not request or request.status != ApprovalStatus.PENDING:
return None
request.status = ApprovalStatus.REJECTED
request.resolved_at = datetime.now()
if self._on_request_resolved:
self._on_request_resolved(request)
return request
def get_pending(self) -> list[ApprovalRequest]:
"""Get all pending requests."""
with self._lock:
return [r for r in self._requests.values() if r.status == ApprovalStatus.PENDING]
def get_request(self, request_id: str) -> Optional[ApprovalRequest]:
"""Get a specific request."""
with self._lock:
return self._requests.get(request_id)
def clear_resolved(self) -> int:
"""Clear all resolved requests. Returns count of cleared requests."""
with self._lock:
to_remove = [
rid for rid, req in self._requests.items()
if req.status != ApprovalStatus.PENDING
]
for rid in to_remove:
del self._requests[rid]
return len(to_remove)
def wait_for_approval(
self,
request_id: str,
timeout: float = 300.0,
check_interval: float = 0.5,
) -> Optional[ApprovalRequest]:
"""
Wait for a request to be approved or rejected.
Args:
request_id: The request ID to wait for
timeout: Maximum time to wait in seconds
check_interval: How often to check status
Returns:
The resolved request, or None if timeout
"""
import time
start = time.time()
while time.time() - start < timeout:
request = self.get_request(request_id)
if not request:
return None
if request.status != ApprovalStatus.PENDING:
return request
time.sleep(check_interval)
# Timeout - mark as expired
with self._lock:
request = self._requests.get(request_id)
if request and request.status == ApprovalStatus.PENDING:
request.status = ApprovalStatus.EXPIRED
request.resolved_at = datetime.now()
return request
# Global approval queue instance
_approval_queue: Optional[ApprovalQueue] = None
def get_approval_queue() -> ApprovalQueue:
"""Get the global approval queue instance."""
global _approval_queue
if _approval_queue is None:
_approval_queue = ApprovalQueue()
return _approval_queue