- 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)
90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
"""FlowDeck — agent governance (v7.2.0): workspace tool scope + approval gate.
|
|
|
|
``agent_policies``: ``allowed_tools_json`` (null = all tools), ``max_steps``,
|
|
``require_approval`` (write tools pause for a human). ``check_tool()`` is
|
|
consulted by ``AgentEngine`` before ``PermissionManager.assert_can``.
|
|
``agent.run.approval_requested`` is emitted on the outbound webhook bus so
|
|
external systems can subscribe. See ``docs/V72_Enterprise_SCIM_2FA.md``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from app.db import get_conn
|
|
|
|
|
|
def get_policy(workspace_id: int | None) -> dict:
|
|
"""Effective policy (workspace row, else global row, else defaults)."""
|
|
with get_conn() as conn:
|
|
row = None
|
|
if workspace_id is not None:
|
|
row = conn.execute("SELECT * FROM agent_policies WHERE workspace_id=?",
|
|
(workspace_id,)).fetchone()
|
|
if row is None:
|
|
row = conn.execute("SELECT * FROM agent_policies WHERE workspace_id IS NULL"
|
|
).fetchone()
|
|
if not row:
|
|
return {"allowed_tools": None, "max_steps": 12, "require_approval": False}
|
|
d = dict(row)
|
|
try:
|
|
allowed = json.loads(d.get("allowed_tools_json")) if d.get("allowed_tools_json") else None
|
|
except (TypeError, ValueError):
|
|
allowed = None
|
|
return {"allowed_tools": allowed, "max_steps": d.get("max_steps") or 12,
|
|
"require_approval": bool(d.get("require_approval"))}
|
|
|
|
|
|
def check_tool(user_id: int, workspace_id: int | None, tool: str,
|
|
is_write: bool, conversation_id: int = 0) -> dict:
|
|
"""Policy gate for one tool call.
|
|
|
|
Returns {allowed: bool, approval_id: int|None}. Denied tools and gated
|
|
writes (pending approval) return allowed=False; the engine renders both
|
|
as action errors without executing.
|
|
"""
|
|
from app.services.permission_manager import WRITE_TOOLS
|
|
policy = get_policy(workspace_id)
|
|
allowed = policy["allowed_tools"]
|
|
if allowed is not None and tool not in set(allowed):
|
|
return {"allowed": False, "approval_id": None, "reason": "tool not in policy scope"}
|
|
if is_write or tool in WRITE_TOOLS:
|
|
if policy["require_approval"]:
|
|
with get_conn() as conn:
|
|
cur = conn.execute(
|
|
"""INSERT INTO agent_approvals
|
|
(conversation_id, tool, args_json, status, requester_id)
|
|
VALUES (?,?,?,?,?)""",
|
|
(conversation_id, tool, "{}", "pending", user_id))
|
|
conn.commit()
|
|
approval_id = cur.lastrowid
|
|
try:
|
|
import asyncio
|
|
|
|
from app.services.webhook_outbound import fire_event as _fire
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
loop = None
|
|
if loop is not None:
|
|
loop.create_task(_fire("agent.run.approval_requested", {
|
|
"approval_id": approval_id, "tool": tool,
|
|
"conversation_id": conversation_id}))
|
|
except Exception: # noqa: BLE001 — webhook never blocks policy
|
|
pass
|
|
return {"allowed": False, "approval_id": approval_id,
|
|
"reason": "approval requested"}
|
|
return {"allowed": True, "approval_id": None, "reason": ""}
|
|
|
|
|
|
def decide_approval(approval_id: int, approver_id: int, approve: bool) -> dict | None:
|
|
with get_conn() as conn:
|
|
row = conn.execute("SELECT * FROM agent_approvals WHERE id=?",
|
|
(approval_id,)).fetchone()
|
|
if not row or row["status"] != "pending":
|
|
return None
|
|
conn.execute("UPDATE agent_approvals SET status=?, approver_id=? WHERE id=?",
|
|
("approved" if approve else "rejected", approver_id, approval_id))
|
|
conn.commit()
|
|
return dict(conn.execute("SELECT * FROM agent_approvals WHERE id=?",
|
|
(approval_id,)).fetchone())
|