"""Agent action approval queue — gate before executing sensitive queries.""" from __future__ import annotations import json from typing import Any, Optional from app.db import execute, fetch_all, fetch_one def _serialize(row: dict[str, Any] | None) -> dict[str, Any] | None: if not row: return None out = dict(row) for key, val in list(out.items()): if hasattr(val, "isoformat"): out[key] = val.isoformat() return out def has_pending_request(agent_key: str, action_type: str | None = None) -> bool: clauses = ["agent_key = %s", "status = 'pending'", "created_at >= CURRENT_DATE"] params: list[Any] = [agent_key.strip().lower()] if action_type: clauses.append("action_type = %s") params.append(action_type) row = fetch_one( f"SELECT id FROM agent_action_requests WHERE {' AND '.join(clauses)} LIMIT 1", tuple(params), ) return bool(row) def create_request( agent_key: str, title: str, action_type: str = "query", query_payload: dict[str, Any] | None = None, ) -> dict[str, Any]: key = (agent_key or "").strip().lower() if not key or not title.strip(): raise ValueError("agent_key and title are required") row = fetch_one( """ INSERT INTO agent_action_requests (agent_key, action_type, title, query_payload, status) VALUES (%s, %s, %s, %s::jsonb, 'pending') RETURNING * """, (key, action_type, title.strip(), json.dumps(query_payload or {})), ) req = _serialize(row) or {} try: execute( """ INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb) """, ( key, "agent_request", "approval_request", title.strip(), f"Wacht op goedkeuring — {action_type}", "needs_approval", "agents", json.dumps({"request_id": req.get("id"), "action_type": action_type}), ), ) except Exception: pass return req def list_requests(status: Optional[str] = None, limit: int = 50) -> list[dict[str, Any]]: clauses, params = [], [] if status: clauses.append("status = %s") params.append(status) where = (" WHERE " + " AND ".join(clauses)) if clauses else "" safe_limit = max(1, min(limit, 200)) rows = fetch_all( f"SELECT * FROM agent_action_requests{where} ORDER BY created_at DESC LIMIT %s", tuple(params + [safe_limit]), ) return [_serialize(r) for r in rows] def get_request(request_id: int) -> dict[str, Any] | None: return _serialize(fetch_one("SELECT * FROM agent_action_requests WHERE id = %s", (request_id,))) def approve_request(request_id: int, approved_by: str = "ceo") -> dict[str, Any]: row = fetch_one( """ UPDATE agent_action_requests SET status = 'approved', approved_by = %s, reviewed_at = NOW() WHERE id = %s AND status = 'pending' RETURNING * """, (approved_by, request_id), ) if not row: raise ValueError("Request not found or not pending") req = _serialize(row) or {} try: execute( """ UPDATE agent_events SET status = 'approved' WHERE status = 'needs_approval' AND metadata->>'request_id' = %s """, (str(request_id),), ) except Exception: pass return req def reject_request(request_id: int, reason: str = "", rejected_by: str = "ceo") -> dict[str, Any]: row = fetch_one( """ UPDATE agent_action_requests SET status = 'rejected', approved_by = %s, rejection_reason = %s, reviewed_at = NOW() WHERE id = %s AND status = 'pending' RETURNING * """, (rejected_by, (reason or "")[:500], request_id), ) if not row: raise ValueError("Request not found or not pending") req = _serialize(row) or {} try: execute( """ UPDATE agent_events SET status = 'rejected' WHERE status = 'needs_approval' AND metadata->>'request_id' = %s """, (str(request_id),), ) except Exception: pass return req def mark_executed(request_id: int, result: dict[str, Any] | None = None) -> dict[str, Any]: row = fetch_one( """ UPDATE agent_action_requests SET status = 'executed', executed_at = NOW(), result = %s::jsonb WHERE id = %s AND status = 'approved' RETURNING * """, (json.dumps(result or {}), request_id), ) if not row: raise ValueError("Request not approved or not found") req = _serialize(row) or {} try: from app.services import projects as project_svc payload = req.get("query_payload") or {} if isinstance(payload, str): import json as _json try: payload = _json.loads(payload) except Exception: payload = {} pid = payload.get("project_id") project_svc.register_agent_output( asset_type=str(req.get("action_type") or "agent_action"), title=req.get("title") or f"Agent actie #{request_id}", ref_id=str(request_id), payload={"result": result or {}, "action_type": req.get("action_type")}, project_id=int(pid) if pid else None, source_agent=str(req.get("agent_key") or "agent"), created_by=str(req.get("approved_by") or "ceo"), ) except Exception: pass return req def require_approved(request_id: int) -> dict[str, Any]: req = get_request(request_id) if not req: raise PermissionError("Approval request not found") if req.get("status") != "approved": raise PermissionError(f"Request status is {req.get('status')}, approval required") return req