"""FlowDeck — agent governance API (v7.2.0): policies + approval queue.""" from __future__ import annotations import json from fastapi import APIRouter, Body, HTTPException, Request from fastapi.responses import JSONResponse from app.auth.session import SessionManager from app.db import get_conn from app.services import agent_policies as policies from app.services.api_v2_helpers import audit_log router = APIRouter(tags=["governance"]) def _owner_or_admin(request: Request) -> dict: user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if not user or not user.get("id"): raise HTTPException(401, "Authentication required") with get_conn() as conn: row = conn.execute("SELECT is_admin FROM users WHERE id=?", (user["id"],)).fetchone() is_admin = bool(row and row["is_admin"]) if not is_admin and request.query_params.get("workspace_id"): member = conn.execute( "SELECT role FROM workspace_members WHERE workspace_id=? AND user_id=?", (request.query_params.get("workspace_id"), user["id"])).fetchone() owner = conn.execute("SELECT id FROM workspaces WHERE id=? AND owner_id=?", (request.query_params.get("workspace_id"), user["id"])).fetchone() if not member and not owner: raise HTTPException(403, "Workspace access required") if member and member["role"] not in ("admin", "editor", "owner"): raise HTTPException(403, "Editor role required") user["is_admin"] = is_admin return user @router.get("/api/v2/agent-policies") def list_policies(request: Request): _owner_or_admin(request) with get_conn() as conn: rows = conn.execute("SELECT * FROM agent_policies ORDER BY workspace_id").fetchall() return {"policies": [dict(r) for r in rows]} @router.post("/api/v2/agent-policies") def upsert_policy(request: Request, body: dict = Body(default={})): user = _owner_or_admin(request) wid = body.get("workspace_id") tools = body.get("allowed_tools") if tools is not None and not isinstance(tools, list): raise HTTPException(400, "allowed_tools must be a list or null") with get_conn() as conn: conn.execute( """INSERT INTO agent_policies (workspace_id, allowed_tools_json, max_steps, require_approval) VALUES (?,?,?,?) ON CONFLICT(workspace_id) DO UPDATE SET allowed_tools_json=excluded.allowed_tools_json, max_steps=excluded.max_steps, require_approval=excluded.require_approval""", (wid, json.dumps(tools) if tools is not None else None, max(1, min(int(body.get("max_steps") or 12), 50)), 1 if body.get("require_approval") else 0)) conn.commit() row = conn.execute("SELECT * FROM agent_policies WHERE workspace_id IS ?", (wid,)).fetchone() audit_log(user, "agent.policy.upsert", "workspace", wid or 0, "", request) return JSONResponse(status_code=201, content=dict(row)) @router.get("/api/v2/agent-approvals") def list_approvals(request: Request): _owner_or_admin(request) status = request.query_params.get("status", "pending") with get_conn() as conn: rows = conn.execute("SELECT * FROM agent_approvals WHERE status=? ORDER BY id DESC" " LIMIT 100", (status,)).fetchall() return {"approvals": [dict(r) for r in rows]} @router.post("/api/v2/agent-approvals/{approval_id}/decide") def decide_approval(approval_id: int, request: Request, body: dict = Body(default={})): user = _owner_or_admin(request) out = policies.decide_approval(approval_id, user["id"], bool(body.get("approve"))) if out is None: raise HTTPException(404, "Pending approval not found") audit_log(user, "agent.approval.decide", "agent_approval", approval_id, out["status"], request) return out