Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.6 KiB
Python
133 lines
3.6 KiB
Python
"""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
|