Compare commits

...

2 Commits

Author SHA1 Message Date
teknium1
c7657974aa feat(skills-hub): unify both science skill repos under one 'science' bucket
Adds an optional tap-level 'bucket' key that stamps a shared hub category on
every skill from a tap when the repo ships no skills.sh.json grouping (the
sidecar still wins when present). Taps both scientific-skill repos under
bucket='science' so they surface together instead of as two unrelated repos:

- K-Dense-AI/scientific-agent-skills (~150, flat skills/<name>/, MIT)
- synthetic-sciences/openscience (~290, nested backend/cli/skills/<cat>/<name>/,
  Apache-2.0) — one tap entry per category since _list_skills_in_repo only
  walks one level under a tap path

Both community trust (NOT in TRUSTED_REPOS): guard scans every skill,
INSTALL_POLICY auto-installs only 'safe'. Overlapping skill names surface from
both repos with distinct identifiers; the user picks the source.

Tests: bucket stamps category with no sidecar; sidecar grouping wins over
bucket. 160/160 test_skills_hub pass.
2026-07-08 06:35:15 -07:00
teknium1
beffeea053 feat(skills-hub): add K-Dense scientific-agent-skills as a default community tap
Registers K-Dense-AI/scientific-agent-skills (~150 MIT-licensed scientific
research skills — ML training/eval, computational biology, cheminformatics,
physics, scientific databases) as a DEFAULT_TAPS entry so the skills are
discoverable and installable via 'hermes skills search/install' without any
per-user tap setup.

Deliberately NOT added to TRUSTED_REPOS: it resolves at 'community' trust, so
the security guard scans every skill and INSTALL_POLICY only auto-installs the
ones rated 'safe'. The skills wrap third-party tools whose own licenses vary
(some GPL; KEGG needs a commercial license for non-academic use) — those terms
are surfaced per-skill and are the installer's concern at use time, not vetted
here. Nothing is vendored into the tree; the upstream repo stays the source.
2026-07-07 16:36:37 -07:00
2 changed files with 101 additions and 6 deletions

View File

@@ -198,6 +198,59 @@ class TestSkillsShGroupings:
assert len(skills) == 1
assert "category" not in skills[0].extra
def test_list_skills_bucket_stamps_category_when_no_sidecar(self):
# A tap-level bucket labels every skill when the repo ships no
# skills.sh.json grouping — this is how several repos share one hub
# category (e.g. science).
auth = MagicMock()
src = GitHubSource(auth=auth)
meta = SkillMeta(
name="rdkit", description="d", source="github",
identifier="K-Dense-AI/scientific-agent-skills/skills/rdkit",
trust_level="community",
)
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = [{"type": "dir", "name": "rdkit"}]
with patch.object(src, "_read_cache", return_value=None), \
patch.object(src, "_write_cache"), \
patch.object(src, "_get_skillsh_groupings", return_value=None), \
patch.object(src, "inspect", return_value=meta), \
patch("tools.skills_hub.httpx.get", return_value=resp):
skills = src._list_skills_in_repo(
"K-Dense-AI/scientific-agent-skills", "skills/", "science"
)
assert len(skills) == 1
assert skills[0].extra["category"] == "science"
def test_list_skills_sidecar_grouping_wins_over_bucket(self):
# When a repo DOES publish a skills.sh.json grouping, that wins over
# the tap-level bucket fallback.
auth = MagicMock()
src = GitHubSource(auth=auth)
meta = SkillMeta(
name="cuopt-developer", description="d", source="github",
identifier="NVIDIA/skills/skills/cuopt-developer", trust_level="trusted",
)
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = [{"type": "dir", "name": "cuopt-developer"}]
with patch.object(src, "_read_cache", return_value=None), \
patch.object(src, "_write_cache"), \
patch.object(src, "_get_skillsh_groupings",
return_value={"cuopt-developer": "Decision Optimization"}), \
patch.object(src, "inspect", return_value=meta), \
patch("tools.skills_hub.httpx.get", return_value=resp):
skills = src._list_skills_in_repo("NVIDIA/skills", "skills/", "science")
assert len(skills) == 1
assert skills[0].extra["category"] == "Decision Optimization"
def test_meta_to_dict_roundtrip_preserves_extra(self):
meta = SkillMeta(
name="x", description="d", source="github",

View File

@@ -525,6 +525,36 @@ class GitHubSource(SkillSource):
# https://github.com/NVIDIA/skills/tree/main/skills
{"repo": "NVIDIA/skills", "path": "skills/"},
{"repo": "garrytan/gstack", "path": ""},
# --- Science bucket -------------------------------------------------
# Two upstream scientific-skill repos are tapped under one shared hub
# category ("science") via the tap-level "bucket" key, so their skills
# surface together instead of as two unrelated repos. Both are
# community trust on purpose — deliberately NOT in
# tools/skills_guard.py::TRUSTED_REPOS — so the security guard scans
# every skill and INSTALL_POLICY only auto-installs "safe" ones.
# Individual skills wrap third-party tools whose OWN licenses vary
# (some GPL; KEGG needs a commercial license for non-academic use);
# those terms are surfaced per-skill (frontmatter/Prerequisites) and
# are the installer's concern at use time, not vetted here.
#
# K-Dense-AI/scientific-agent-skills: ~150 skills, flat skills/<name>/
# layout (matches the anthropic/huggingface taps). MIT-licensed content.
{"repo": "K-Dense-AI/scientific-agent-skills", "path": "skills/", "bucket": "science"},
# synthetic-sciences/openscience: ~290 skills, Apache-2.0. Nested
# backend/cli/skills/<category>/<name>/ layout — _list_skills_in_repo
# only walks ONE level under a tap path, so each category is tapped
# separately (same one-entry-per-inner-path pattern as openai above).
*(
{"repo": "synthetic-sciences/openscience",
"path": f"backend/cli/skills/{_cat}/", "bucket": "science"}
for _cat in (
"biology", "chemistry", "cloud-compute", "coding",
"data-engineering", "databases", "document-parsing",
"llm-tools", "ml-inference", "ml-training", "other",
"physics", "quantum", "research", "scholar-evaluation",
"visualization", "writing",
)
),
]
def __init__(self, auth: GitHubAuth, extra_taps: Optional[List[Dict]] = None):
@@ -566,7 +596,7 @@ class GitHubSource(SkillSource):
for tap in self.taps:
try:
skills = self._list_skills_in_repo(tap["repo"], tap.get("path", ""))
skills = self._list_skills_in_repo(tap["repo"], tap.get("path", ""), tap.get("bucket"))
for skill in skills:
searchable = f"{skill.name} {skill.description} {' '.join(skill.tags)}".lower()
if query_lower in searchable:
@@ -663,9 +693,15 @@ class GitHubSource(SkillSource):
# -- Internal helpers --
def _list_skills_in_repo(self, repo: str, path: str) -> List[SkillMeta]:
"""List skill directories in a GitHub repo path, using cached index."""
cache_key = f"{repo}_{path}".replace("/", "_").replace(" ", "_")
def _list_skills_in_repo(self, repo: str, path: str, bucket: Optional[str] = None) -> List[SkillMeta]:
"""List skill directories in a GitHub repo path, using cached index.
``bucket`` optionally forces a category label on every skill from this
tap (used to unify skills from several repos under one hub category,
e.g. multiple science repos → "science"). A repo's own
``skills.sh.json`` grouping still wins when present.
"""
cache_key = f"{repo}_{path}_{bucket or ''}".replace("/", "_").replace(" ", "_")
cached = self._read_cache(cache_key)
if cached is not None:
return [SkillMeta(**s) for s in cached]
@@ -693,10 +729,16 @@ class GitHubSource(SkillSource):
skill_identifier = f"{repo}/{prefix}/{dir_name}" if prefix else f"{repo}/{dir_name}"
meta = self.inspect(skill_identifier)
if meta:
category = None
if groupings:
category = groupings.get(meta.name) or groupings.get(dir_name)
if category:
meta.extra["category"] = category
# Tap-level bucket is the fallback when the repo ships no
# skills.sh.json grouping — lets several repos share one
# hub category (e.g. science) without a sidecar we can't edit.
if not category and bucket:
category = bucket
if category:
meta.extra["category"] = category
skills.append(meta)
# Cache the results