Fix fabric path search, read scope, orbit total ranking, and edge menu clamp.

Prefer BFS shortest paths, allow ne:read on path find, honor objective=total clearance trades, and keep context menus on-screen without a11y clock spam.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 21:31:50 +08:00
parent 298859e608
commit e4a135ec16
13 changed files with 163 additions and 30 deletions

View file

@ -141,6 +141,9 @@ def required_scope_for_request(method: str, path: str) -> str | None:
return SCOPE_NE_READ
if p.startswith("/v1/topology"):
# Read-only path search uses POST for a structured body.
if m == "POST" and p.rstrip("/").endswith("/fabric/paths"):
return SCOPE_NE_READ
if m in ("POST", "PUT", "PATCH", "DELETE"):
return SCOPE_NE_WRITE
return SCOPE_NE_READ

View file

@ -47,6 +47,9 @@ def validate_select_sql(sql: str, *, allowed_tables: set[str] | None = None) ->
low = cleaned.lower().lstrip()
if not low.startswith(("select", "with")):
raise HTTPException(status_code=400, detail="select_only")
# Plain WITH CTE is allowed; RECURSIVE can explode into DoS.
if re.search(r"\bwith\s+recursive\b", cleaned, flags=re.IGNORECASE):
raise HTTPException(status_code=400, detail="with_recursive_not_allowed")
if _SQL_FORBIDDEN_RE.search(cleaned):
raise HTTPException(status_code=400, detail="forbidden_keyword")
# Block obvious catalog / other-schema probes in the text.

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import re
from collections import deque
from datetime import datetime, timedelta
from typing import Any
from uuid import uuid4
@ -509,25 +510,26 @@ def find_fabric_paths(
adj.setdefault(e.a_node_id, []).append((e.b_node_id, e.id))
adj.setdefault(e.b_node_id, []).append((e.a_node_id, e.id))
# DFS for simple paths (no repeated nodes), capped to avoid blow-up on large graphs.
_EXPLORE_CAP = 100
all_paths: list[list[str]] = []
stack: list[tuple[str, list[str], set[str]]] = [(from_id, [], {from_id})]
while stack and len(all_paths) < _EXPLORE_CAP:
node, edge_path, visited = stack.pop()
# BFS for simple paths so shorter hops are found first; cap expansions on dense graphs.
_EXPLORE_CAP = 5000
found: list[list[str]] = []
queue: deque[tuple[str, list[str], set[str]]] = deque([(from_id, [], {from_id})])
explored = 0
while queue and len(found) < max_paths and explored < _EXPLORE_CAP:
node, edge_path, visited = queue.popleft()
if len(edge_path) >= max_hops:
continue
for nbr, eid in adj.get(node, []):
if nbr in visited:
continue
explored += 1
new_path = edge_path + [eid]
if nbr == to_id:
all_paths.append(new_path)
found.append(new_path)
if len(found) >= max_paths:
break
continue
stack.append((nbr, new_path, visited | {nbr}))
all_paths.sort(key=len)
found = all_paths[:max_paths]
queue.append((nbr, new_path, visited | {nbr}))
node_ids = {from_id, to_id}
for p in found:

View file

@ -26,6 +26,7 @@ from .topology_fabric import (
ensure_fabric_node_for_managed,
ensure_fabric_node_for_ume,
ensure_lldp_discovered_managed_ne,
find_fabric_paths,
get_fabric_neighborhood,
get_fabric_summary,
list_fabric_edges,