fix(skills): preserve trust for skills-sh identifiers + reduce resolution churn (#3251)

* fix(skills): reduce skills.sh resolution churn and preserve trust for wrapped identifiers

- Accept common skills.sh prefix typos (skils-sh/, skils.sh/)
- Strip skills-sh/ prefix in _resolve_trust_level() so trusted repos
  stay trusted when installed through skills.sh
- Use resolved identifier (from bundle/meta) for scan_skill source
- Prefer tree search before root scan in _discover_identifier()
- Add _resolve_github_meta() consolidation for inspect flow

Cherry-picked from PR #3001 by kshitijk4poor.

* fix: restore candidate loop in SkillsShSource.fetch() for consistency

The cherry-picked PR only tried the first candidate identifier in
fetch() while inspect() (via _resolve_github_meta) tried all four.
This meant skills at repo/skills/path would be found by inspect but
missed by fetch, forcing it through the heavier _discover_identifier
flow. Restore the candidate loop so both paths behave identically.

Updated the test assertion to match.

---------

Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
This commit is contained in:
Teknium
2026-03-26 13:40:21 -07:00
committed by GitHub
parent 62f8aa9b03
commit b7b3294c4a
6 changed files with 209 additions and 28 deletions

View File

@@ -1050,15 +1050,27 @@ def _get_configured_model() -> str:
def _resolve_trust_level(source: str) -> str:
"""Map a source identifier to a trust level."""
prefix_aliases = (
"skills-sh/",
"skills.sh/",
"skils-sh/",
"skils.sh/",
)
normalized_source = source
for prefix in prefix_aliases:
if normalized_source.startswith(prefix):
normalized_source = normalized_source[len(prefix):]
break
# Agent-created skills get their own permissive trust level
if source == "agent-created":
if normalized_source == "agent-created":
return "agent-created"
# Official optional skills shipped with the repo
if source.startswith("official/") or source == "official":
if normalized_source.startswith("official/") or normalized_source == "official":
return "builtin"
# Check if source matches any trusted repo
for trusted in TRUSTED_REPOS:
if source.startswith(trusted) or source == trusted:
if normalized_source.startswith(trusted) or normalized_source == trusted:
return "trusted"
return "community"

View File

@@ -925,19 +925,10 @@ class SkillsShSource(SkillSource):
def inspect(self, identifier: str) -> Optional[SkillMeta]:
canonical = self._normalize_identifier(identifier)
detail: Optional[dict] = None
for candidate in self._candidate_identifiers(canonical):
meta = self.github.inspect(candidate)
if meta:
detail = self._fetch_detail_page(canonical)
return self._finalize_inspect_meta(meta, canonical, detail)
detail = self._fetch_detail_page(canonical)
resolved = self._discover_identifier(canonical, detail=detail)
if resolved:
meta = self.github.inspect(resolved)
if meta:
return self._finalize_inspect_meta(meta, canonical, detail)
meta = self._resolve_github_meta(canonical, detail=detail)
if meta:
return self._finalize_inspect_meta(meta, canonical, detail)
return None
def _featured_skills(self, limit: int) -> List[SkillMeta]:
@@ -1099,6 +1090,13 @@ class SkillsShSource(SkillSource):
if self._matches_skill_tokens(meta, tokens):
return meta.identifier
# Prefer a single recursive tree lookup before brute-forcing every
# top-level directory. This avoids large request bursts on categorized
# repos like borghei/claude-skills.
tree_result = self.github._find_skill_in_repo_tree(repo, skill_token)
if tree_result:
return tree_result
# Fallback: scan repo root for directories that might contain skills
try:
root_url = f"https://api.github.com/repos/{repo}/contents/"
@@ -1131,14 +1129,17 @@ class SkillsShSource(SkillSource):
except Exception:
pass
# Final fallback: use the GitHub Trees API to find the skill anywhere
# in the repo tree. This handles deeply nested structures like
# cli-tool/components/skills/development/<skill>/ that the shallow
# scan above can't reach.
tree_result = self.github._find_skill_in_repo_tree(repo, skill_token)
if tree_result:
return tree_result
return None
def _resolve_github_meta(self, identifier: str, detail: Optional[dict] = None) -> Optional[SkillMeta]:
for candidate in self._candidate_identifiers(identifier):
meta = self.github.inspect(candidate)
if meta:
return meta
resolved = self._discover_identifier(identifier, detail=detail)
if resolved:
return self.github.inspect(resolved)
return None
def _finalize_inspect_meta(self, meta: SkillMeta, canonical: str, detail: Optional[dict]) -> SkillMeta:
@@ -1264,10 +1265,15 @@ class SkillsShSource(SkillSource):
@staticmethod
def _normalize_identifier(identifier: str) -> str:
if identifier.startswith("skills-sh/"):
return identifier[len("skills-sh/"):]
if identifier.startswith("skills.sh/"):
return identifier[len("skills.sh/"):]
prefix_aliases = (
"skills-sh/",
"skills.sh/",
"skils-sh/",
"skils.sh/",
)
for prefix in prefix_aliases:
if identifier.startswith(prefix):
return identifier[len(prefix):]
return identifier
@staticmethod