"""pfSense diagnostics — issue-aware playbook selection.""" from __future__ import annotations import re from typing import Awaitable, Callable from app.agent.tool_runner import invoke_connect, invoke_tool EmitFn = Callable[[int, str, str, dict | None], Awaitable[None]] PFSENSE_MCP = "pfsense-mcp" ArgBuilder = Callable[[dict], dict] PFSENSE_DEFAULT_DIAGNOSTICS: list[tuple[str, ArgBuilder]] = [ ("pfsense_get_system_status", lambda _c: {}), ("pfsense_list_interfaces", lambda _c: {}), ("pfsense_list_gateways", lambda _c: {}), ("pfsense_get_gateway_status", lambda _c: {}), ] PFSENSE_FIREWALL_DIAGNOSTICS: list[tuple[str, ArgBuilder]] = [ ("pfsense_list_interfaces", lambda _c: {}), ("pfsense_list_firewall_rules", lambda _c: {"limit": 250}), ("pfsense_list_port_forwards", lambda _c: {"limit": 100}), ("pfsense_list_outbound_nat_mappings", lambda _c: {"limit": 100}), ("pfsense_list_aliases", lambda _c: {"limit": 100}), ] _FIREWALL_TERMS = ( "firewall", "rule", "rules", "policy", "policies", "filter rule", "nat", "port forward", "port-forward", "outbound nat", "inbound", "acl", ) def _text_has_term(text: str, term: str) -> bool: term = term.lower().strip() if not term: return False if " " in term: return term in text return bool(re.search(rf"\b{re.escape(term)}\b", text)) def wants_firewall_inventory(title: str, issue: str) -> bool: text = f"{title} {issue}".lower() return any(_text_has_term(text, term) for term in _FIREWALL_TERMS) def diagnostics_for_issue(title: str, issue: str) -> list[tuple[str, ArgBuilder]]: if wants_firewall_inventory(title, issue): return list(PFSENSE_FIREWALL_DIAGNOSTICS) return list(PFSENSE_DEFAULT_DIAGNOSTICS) async def run_pfsense_diagnostics( task_id: int, device: dict, manager, emit: EmitFn, *, title: str = "", issue: str = "", max_diagnostics: int = 8, ) -> list[dict]: """Connect to pfSense and run read-only diagnostics matched to the task.""" from app.agent.device_connect import pfsense_connect_args results: list[dict] = [] label = device.get("catalog_key") or device.get("name") or "pfsense" server = PFSENSE_MCP server_state = manager.servers.get(server or "") if not server_state or server_state.status != "loaded": await emit( task_id, "diagnose", f"MCP '{server}' not available for {label}.", {"device": label, "server": server}, ) return results diagnostics = diagnostics_for_issue(title, issue) intent = "firewall inventory" if wants_firewall_inventory(title, issue) else "health" await emit( task_id, "connect", f"Connecting to {label} via {server} ({intent})...", {"device": label, "intent": intent}, ) ok = await invoke_connect( task_id, label, server, "pfsense_connect", pfsense_connect_args(device), manager, emit ) if not ok: return results for tool, builder in diagnostics[:max_diagnostics]: args = builder(device) res, tool_ok = await invoke_tool( task_id, label, server, tool, args, manager, emit, task_title=title, task_issue=issue, ) results.append( { "device": label, "tool": tool, "ok": tool_ok, "output": res.get("text", ""), "intent": intent, } ) return results