""" Tool settings router. Routes: GET /settings/tools → tool risk policy page POST /settings/tools → save max_risk + per-tool overrides """ import html as _html import json import logging from pathlib import Path import jwt from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from auth_utils import COOKIE_NAME, decode_token, get_tool_policy, save_tool_policy from persona import list_user_personas from tools import TOOL_CATEGORIES, TOOL_RISK logger = logging.getLogger(__name__) router = APIRouter() _STATIC = Path(__file__).parent.parent / "static" _LAST_PERSONA_COOKIE = "cx_last_persona" def _get_session_user(request: Request) -> str | None: token = request.cookies.get(COOKIE_NAME) if not token: return None try: return decode_token(token) except jwt.InvalidTokenError: return None def _preferred_persona(request: Request, username: str) -> str: names = list_user_personas(username) if not names: return "" cookie_val = request.cookies.get(_LAST_PERSONA_COOKIE, "") return cookie_val if cookie_val in names else (names[0] if names else "") def _build_tool_table(policy: dict) -> str: """Generate the per-tool override table rows grouped by category.""" whitelist = set(policy.get("whitelist") or []) blacklist = set(policy.get("blacklist") or []) rows: list[str] = [] for category, tools in TOOL_CATEGORIES.items(): # Category header spanning all columns escaped_cat = _html.escape(category) rows.append( f'' f'{escaped_cat}' ) for tool in tools: risk = TOOL_RISK.get(tool, "medium") risk_cls = f"risk-{risk}" risk_html = f'{_html.escape(risk)}' # Override select value if tool in whitelist: override_val = "whitelist" elif tool in blacklist: override_val = "blacklist" else: override_val = "default" def _opt(val: str, label: str) -> str: sel = 'selected' if override_val == val else '' return f'' override_sel = ( f'' ) rows.append( f'' f'{_html.escape(tool)}' f'{risk_html}' f'' f'{override_sel}' f'' ) table_body = "\n".join(rows) return ( '' '' '' '' f'{table_body}' '
ToolRiskAuto statusOverride
' ) def _tools_page( username: str, back_persona: str = "", success: str = "", error: str = "", ) -> str: html = (_STATIC / "tools_settings.html").read_text() policy = get_tool_policy(username) max_risk = policy.get("max_risk") or "" # Max risk select options html = html.replace("{{ sel_none }}", "selected" if max_risk == "" else "") html = html.replace("{{ sel_low }}", "selected" if max_risk == "low" else "") html = html.replace("{{ sel_medium }}", "selected" if max_risk == "medium" else "") html = html.replace("{{ sel_high }}", "selected" if max_risk == "high" else "") html = html.replace("{{ tool_table_html }}", _build_tool_table(policy)) html = html.replace("{{ tool_risk_json }}", json.dumps(TOOL_RISK)) html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/") html = html.replace("{{ help_href }}", f"/help?persona={back_persona}" if back_persona else "/help") if success: html = html.replace("", f'

{success}

') if error: html = html.replace("", f'

{error}

') return html @router.get("/settings/tools", include_in_schema=False) async def tools_page(request: Request): username = _get_session_user(request) if not username: return RedirectResponse("/login", status_code=302) back_persona = _preferred_persona(request, username) return HTMLResponse(_tools_page(username, back_persona)) @router.post("/settings/tools", include_in_schema=False) async def save_tools(request: Request): username = _get_session_user(request) if not username: return RedirectResponse("/login", status_code=302) back_persona = _preferred_persona(request, username) form = await request.form() max_risk = (form.get("max_risk") or "").strip() if max_risk not in ("", "low", "medium", "high"): max_risk = "" whitelist: list[str] = [] blacklist: list[str] = [] all_tools = [t for tools in TOOL_CATEGORIES.values() for t in tools] for tool in all_tools: val = (form.get(f"override_{tool}") or "").strip() if val == "whitelist": whitelist.append(tool) elif val == "blacklist": blacklist.append(tool) # Merge into existing policy (preserve allow/deny confirmation-gate fields) policy = get_tool_policy(username) if max_risk: policy["max_risk"] = max_risk else: policy.pop("max_risk", None) policy["whitelist"] = whitelist policy["blacklist"] = blacklist save_tool_policy(username, policy) logger.info( "tool policy saved for %s: max_risk=%s whitelist=%d blacklist=%d", username, max_risk or "none", len(whitelist), len(blacklist), ) return HTMLResponse(_tools_page( username, back_persona, success=f"Tool policy saved — max risk: {max_risk or 'none'}, " f"{len(whitelist)} whitelisted, {len(blacklist)} blacklisted.", ))