4 passes (283 → 93 routes async sur 667 = 86 % hors loop, avant 61 %) :
A. RACINE AUTH — `get_current_user` (auth/session.py) était `async def`
SANS aucun await (cookie decode = synchrone) ; idem ses clones :
`agent._current_user_id/_workspace_id/_current_admin` (34 sites) et
`sso._require_admin` (corps 0 await, 6 sites) → `def` +
47 `await` supprimés. Piège : 3 call sites passaient par l'alias `gcu`
(grep littéral aveugle) — 8 tests en échec → corrigés.
B. Re-scan : 19 routes devenues SANS await → `def` (agent 8, sso 5,
web_clipper 3, projects 2, auth 1…).
C/D. 155 routes dont les seuls awaits = `request.json()` / événements :
- try/except `body = {}` → `Body(default={})` (même tolérance)
- try/except `raise HTTPException(400)` → `Body(...)` REQUIS
(422 FastAPI — aucun test ne couvrait le 400)
- forme conditionnelle `request.json() if content-type else {}`
(54 sites) → défaut `{}` (sans corps = `{}` dans les 2 cas)
- `await fire_*` → `run_event_sync(...)` ; imports `Body` /
`run_event_sync` ajoutés aux routers convertis
Reste async (93, justifié) : form/upload/file (22), réseau gitea/llm/oidc,
`_json_body` (9), 2 JSON inline en argument, 1 fallback logique
(capture_frontend_error), 1 lecture conditionnelle (web_clipper), mixtes.
suite **1089/1089** · ruff OK · docs à jour
91 lines
4.0 KiB
Python
91 lines
4.0 KiB
Python
"""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
|