feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""FlowDeck — agent governance API (v7.2.0): policies + approval queue."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, 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")
|
||||
async 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")
|
||||
async def upsert_policy(request: Request):
|
||||
user = _owner_or_admin(request)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
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")
|
||||
async 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")
|
||||
async def decide_approval(approval_id: int, request: Request):
|
||||
user = _owner_or_admin(request)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
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
|
||||
Reference in New Issue
Block a user