Some checks failed
plugin-tests / test (push) Failing after 6s
Per sot/03-PROTOCOLS/CORTEX-OS-UMBRELLA-VIZ-PRD.md. Living-graph panel
inside hermes-webui consuming curator-maintained graph/umbrella.json
(UI-stable v1.0 schema emitted by curator-planb sweep.py v0.2).
routes/umbrella.py — 3 endpoints:
GET /umbrella → redirect to static panel HTML
GET /api/umbrella → graph/umbrella.json contents
GET /api/umbrella/doc?path=X → markdown body for a node's source_path
(path-traversal guarded; 50KB cap)
static/umbrella.{html,css,js} — Cytoscape.js v3.30.2 (CDN) render:
- 8 node types: doc/profile/skill/mcp_server/sovereign_api/cortex_tool/
external_dep/credential — each gets distinct color + shape
- 5 edge types: depends_on/governs/consumes/produces/supersedes
- filter chips (toggle node type visibility)
- layout switcher (force/tier/concentric)
- search (dim non-matching nodes)
- click node → side panel w/ frontmatter + markdown body + in/out edges
- edge-list buttons jump between connected nodes
Plan B brand-aligned dark theme; DESIGN.md 8-property subset
(backgroundColor/textColor/typography/rounded/padding/size/height/width).
svrnty_nav.js bundled (sidebar nav integration — non-umbrella scope).
CONNECTION-MAP.md regenerated via scripts/ast-connection-map.py.
plugin.py route-list extended w/ "umbrella" + injects umbrella assets via
the standard static dir registration.
Module import: ✓ (python3 -c "import routes.umbrella" clean).
Graph artifact verified: 81 nodes / 120 edges live in graph/umbrella.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
"""GET /umbrella + /api/umbrella + /api/umbrella/doc — cortex-os umbrella graph viz.
|
|
|
|
Per sot/03-PROTOCOLS/CORTEX-OS-UMBRELLA-VIZ-PRD.md.
|
|
|
|
Endpoints:
|
|
GET /umbrella → static HTML page (Cytoscape.js panel)
|
|
GET /api/umbrella → graph/umbrella.json contents (UI-stable v1.0 schema)
|
|
GET /api/umbrella/doc?path=X → markdown body + frontmatter for a node's source_path
|
|
|
|
Reads graph artifact from $HERMES_REPO_ROOT/graph/umbrella.json (curator-maintained).
|
|
Doc reads are restricted to hermes/ workspace subdir (no path traversal).
|
|
|
|
Public API surface used: api.register_route, api.logger.
|
|
No upstream forced internal dependencies.
|
|
"""
|
|
import json
|
|
import os
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
|
|
_DEFAULT_REPO_ROOT = "/home/svrnty/workspaces/hermes"
|
|
|
|
|
|
def _repo_root() -> Path:
|
|
return Path(os.environ.get("HERMES_REPO_ROOT", _DEFAULT_REPO_ROOT)).resolve()
|
|
|
|
|
|
def register(api):
|
|
"""Wire umbrella panel + APIs."""
|
|
log = api.logger("svrnty.routes.umbrella")
|
|
api.register_route("/umbrella", "GET", _handle_panel_html)
|
|
api.register_route("/api/umbrella", "GET", _handle_graph_json)
|
|
api.register_route("/api/umbrella/doc", "GET", _handle_doc_body)
|
|
log.info("umbrella panel + APIs registered")
|
|
|
|
|
|
def _send(handler, status: int, body: bytes, content_type: str) -> None:
|
|
handler.send_response(status)
|
|
handler.send_header("Content-Type", content_type)
|
|
handler.send_header("Content-Length", str(len(body)))
|
|
handler.send_header("Cache-Control", "no-store")
|
|
handler.end_headers()
|
|
handler.wfile.write(body)
|
|
|
|
|
|
def _handle_graph_json(handler, parsed) -> bool:
|
|
graph_path = _repo_root() / "graph" / "umbrella.json"
|
|
if not graph_path.exists():
|
|
body = json.dumps({
|
|
"error": "graph/umbrella.json not found",
|
|
"hint": "run curator/sweep.py --grapher-only to generate",
|
|
}).encode("utf-8")
|
|
_send(handler, 404, body, "application/json; charset=utf-8")
|
|
return True
|
|
try:
|
|
body = graph_path.read_bytes()
|
|
except OSError as e:
|
|
body = json.dumps({"error": str(e)}).encode("utf-8")
|
|
_send(handler, 500, body, "application/json; charset=utf-8")
|
|
return True
|
|
_send(handler, 200, body, "application/json; charset=utf-8")
|
|
return True
|
|
|
|
|
|
def _handle_doc_body(handler, parsed) -> bool:
|
|
qs = urllib.parse.parse_qs(parsed.query or "")
|
|
rel = (qs.get("path") or [""])[0].strip()
|
|
if not rel:
|
|
_send(handler, 400, b'{"error":"missing path query param"}', "application/json; charset=utf-8")
|
|
return True
|
|
root = _repo_root()
|
|
try:
|
|
target = (root / rel).resolve()
|
|
except (OSError, RuntimeError):
|
|
_send(handler, 400, b'{"error":"invalid path"}', "application/json; charset=utf-8")
|
|
return True
|
|
# Path-traversal guard: resolved target must be inside the workspace root.
|
|
if root not in target.parents and target != root:
|
|
_send(handler, 403, b'{"error":"path outside workspace"}', "application/json; charset=utf-8")
|
|
return True
|
|
if not target.exists() or not target.is_file():
|
|
_send(handler, 404, json.dumps({"error": f"file not found: {rel}"}).encode("utf-8"),
|
|
"application/json; charset=utf-8")
|
|
return True
|
|
try:
|
|
content = target.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeDecodeError) as e:
|
|
_send(handler, 500, json.dumps({"error": str(e)}).encode("utf-8"),
|
|
"application/json; charset=utf-8")
|
|
return True
|
|
# Cap response size — UI only renders a preview pane; large docs read in full via git.
|
|
MAX = 50_000
|
|
truncated = len(content) > MAX
|
|
if truncated:
|
|
content = content[:MAX] + "\n\n*[truncated — read full file in repo]*"
|
|
body = json.dumps({
|
|
"path": rel,
|
|
"size": len(content),
|
|
"truncated": truncated,
|
|
"body": content,
|
|
}).encode("utf-8")
|
|
_send(handler, 200, body, "application/json; charset=utf-8")
|
|
return True
|
|
|
|
|
|
def _handle_panel_html(handler, parsed) -> bool:
|
|
"""Serve the static umbrella.html via Location redirect (assets live in static/)."""
|
|
handler.send_response(302)
|
|
handler.send_header("Location", "/plugins/svrnty/umbrella.html")
|
|
handler.end_headers()
|
|
return True
|