feat(dashboard): rehaul Skills hub browser — connected hubs, featured, preview + security scan (#40384)

The Browse-hub tab was a blank search box with sparse result cards (name +
source + one Install button), no way to read a skill before installing, no
visual security scan, and no indication it was even connected to any hubs.

Backend (web_server.py):
- GET /api/skills/hub/sources — lists the configured hubs (label + trust
  tier + GitHub rate-limit + index availability) and featured skills pulled
  from the centralized index (zero extra API calls), plus installed-skill
  provenance so the UI can mark already-installed results.
- GET /api/skills/hub/preview — fetches a skill's SKILL.md text + file
  manifest WITHOUT installing (decodes byte-stored text, masks binaries).
- GET /api/skills/hub/scan — runs the SAME quarantine + scan_skill +
  should_allow_install pipeline the CLI installer uses, then cleans up
  quarantine, returning verdict / per-finding detail / severity tally /
  install-policy decision.
- search now returns per-source counts + timed-out sources + installed map.

Frontend (SkillsPage HubBrowser):
- Landing state: connected-hubs strip + featured skill grid (no more blank
  page).
- Rich cards: trust-level color coding, source, tags, identifier,
  Details + Install (or Installed state).
- Detail dialog: read the actual SKILL.md, on-demand visual security scan
  (verdict pill, severity tally, per-finding list, allow/block policy),
  GitHub repo link.
- Search meta line: result count + timing + per-source breakdown (the
  'feels slow / no feedback' complaint).

Tests: 4 new endpoint test classes (sources/preview/scan + updated search
shape) in test_dashboard_admin_endpoints.py.
This commit is contained in:
Teknium
2026-06-06 02:44:50 -07:00
committed by GitHub
parent 5af899c7ca
commit 56236b16e3
4 changed files with 1282 additions and 73 deletions
@@ -352,9 +352,212 @@ class TestSkillsHubSearchEndpoint:
self.client, _ = _client()
def test_empty_query_returns_empty(self):
# Empty query short-circuits (no network) and returns no results.
# Empty query short-circuits (no network) and returns the enriched
# empty shape (results + per-source counts + timeouts + installed map).
r = self.client.get("/api/skills/hub/search?q=")
assert r.status_code == 200 and r.json() == {"results": []}
assert r.status_code == 200
body = r.json()
assert body["results"] == []
assert body["source_counts"] == {}
assert body["timed_out"] == []
assert body["installed"] == {}
class _FakeMeta:
"""Minimal SkillMeta stand-in for monkeypatched source search."""
def __init__(self, identifier, trust_level="community", source="github"):
self.name = identifier.rsplit("/", 1)[-1]
self.description = "desc"
self.source = source
self.identifier = identifier
self.trust_level = trust_level
self.repo = "owner/repo"
self.tags = ["a", "b"]
# Used by the preview endpoint's getattr() fallbacks.
self.files = {}
class _FakeBundle:
def __init__(self, identifier, source="github", trust_level="community"):
self.name = identifier.rsplit("/", 1)[-1]
self.identifier = identifier
self.source = source
self.trust_level = trust_level
self.description = "desc"
self.repo = "owner/repo"
self.tags = ["a", "b"]
# Mix str + bytes to exercise the decode-or-placeholder branch.
self.files = {
"SKILL.md": b"---\nname: x\n---\nbody text",
"icon.png": b"\xff\xd8\xff\xe0binary",
"notes.txt": "plain string content",
}
self.metadata = {}
class TestSkillsHubSourcesEndpoint:
@pytest.fixture(autouse=True)
def _setup(self, _isolate_hermes_home):
self.client, _ = _client()
def test_sources_lists_configured_hubs(self, monkeypatch):
# The endpoint should enumerate the configured hub sources without
# requiring any live network — monkeypatch the router.
class _Src:
is_available = False
def __init__(self, sid):
self._sid = sid
def source_id(self):
return self._sid
def search(self, q, limit=10):
return [_FakeMeta("hermes-index/featured-skill", "trusted")]
def _fake_router():
srcs = [_Src("official"), _Src("github")]
# hermes-index source advertises availability + featured search.
idx = _Src("hermes-index")
idx.is_available = True
srcs.insert(1, idx)
return srcs
monkeypatch.setattr(
"tools.skills_hub.create_source_router", _fake_router
)
r = self.client.get("/api/skills/hub/sources")
assert r.status_code == 200
body = r.json()
ids = {s["id"] for s in body["sources"]}
assert {"official", "github", "hermes-index"} <= ids
# Every source carries a human label.
assert all(s.get("label") for s in body["sources"])
assert body["index_available"] is True
# Featured pulled from the index (zero extra API calls).
assert len(body["featured"]) == 1
assert body["featured"][0]["trust_level"] == "trusted"
assert isinstance(body["installed"], dict)
class TestSkillsHubPreviewEndpoint:
@pytest.fixture(autouse=True)
def _setup(self, _isolate_hermes_home):
self.client, _ = _client()
def test_preview_requires_identifier(self):
r = self.client.get("/api/skills/hub/preview?identifier=")
assert r.status_code == 400
def test_preview_returns_skill_md_text(self, monkeypatch):
monkeypatch.setattr(
"tools.skills_hub.create_source_router", lambda: []
)
bundle = _FakeBundle("github/owner/repo/x")
meta = _FakeMeta("github/owner/repo/x")
monkeypatch.setattr(
"hermes_cli.skills_hub._resolve_source_meta_and_bundle",
lambda ident, sources: (meta, bundle, None),
)
r = self.client.get(
"/api/skills/hub/preview?identifier=github/owner/repo/x"
)
assert r.status_code == 200
body = r.json()
# Bytes-stored SKILL.md decodes to text.
assert "body text" in body["skill_md"]
# Binary file is masked, text files decode.
assert "icon.png" in body["files"]
assert sorted(body["files"]) == ["SKILL.md", "icon.png", "notes.txt"]
def test_preview_404_when_unresolved(self, monkeypatch):
monkeypatch.setattr(
"tools.skills_hub.create_source_router", lambda: []
)
monkeypatch.setattr(
"hermes_cli.skills_hub._resolve_source_meta_and_bundle",
lambda ident, sources: (None, None, None),
)
r = self.client.get("/api/skills/hub/preview?identifier=nope/x")
assert r.status_code == 404
class TestSkillsHubScanEndpoint:
@pytest.fixture(autouse=True)
def _setup(self, _isolate_hermes_home):
self.client, _ = _client()
def test_scan_requires_identifier(self):
r = self.client.get("/api/skills/hub/scan?identifier=")
assert r.status_code == 400
def test_scan_returns_verdict_and_policy(self, monkeypatch):
from tools.skills_guard import ScanResult, Finding
monkeypatch.setattr(
"tools.skills_hub.create_source_router", lambda: []
)
bundle = _FakeBundle("github/owner/repo/x", trust_level="community")
monkeypatch.setattr(
"hermes_cli.skills_hub._resolve_source_meta_and_bundle",
lambda ident, sources: (None, bundle, None),
)
from pathlib import Path
monkeypatch.setattr(
"tools.skills_hub.quarantine_bundle", lambda b: Path("/tmp/_fake_q")
)
fake_result = ScanResult(
skill_name="x",
source="github/owner/repo/x",
trust_level="community",
verdict="caution",
findings=[
Finding(
pattern_id="p",
severity="high",
category="exfiltration",
file="SKILL.md",
line=10,
match="m",
description="leaks data",
)
],
summary="s",
)
monkeypatch.setattr(
"tools.skills_guard.scan_skill",
lambda path, source="community": fake_result,
)
# Avoid touching the filesystem during cleanup.
monkeypatch.setattr("shutil.rmtree", lambda *a, **k: None)
r = self.client.get(
"/api/skills/hub/scan?identifier=github/owner/repo/x"
)
assert r.status_code == 200
body = r.json()
assert body["verdict"] == "caution"
assert body["trust_level"] == "community"
# community + caution => blocked by install policy.
assert body["policy"] == "block"
assert body["severity_counts"]["high"] == 1
assert body["findings"][0]["category"] == "exfiltration"
assert body["findings"][0]["file"] == "SKILL.md"
def test_scan_404_when_no_bundle(self, monkeypatch):
monkeypatch.setattr(
"tools.skills_hub.create_source_router", lambda: []
)
monkeypatch.setattr(
"hermes_cli.skills_hub._resolve_source_meta_and_bundle",
lambda ident, sources: (None, None, None),
)
r = self.client.get("/api/skills/hub/scan?identifier=nope/x")
assert r.status_code == 404