Files
Agentic-OS/backend/app/agent/pfsense_profiles.py
nearxos 6185b9b85a Initial commit: Agentic OS troubleshooting platform
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>
2026-06-14 22:11:07 +03:00

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