mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix: harden skill trust source matching (#31229)
Co-authored-by: gaia <gaia@gaia.local>
This commit is contained in:
parent
2d422720b5
commit
93660643a6
4 changed files with 78 additions and 8 deletions
|
|
@ -550,7 +550,14 @@ def do_install(identifier: str, category: str = "", force: bool = False,
|
|||
|
||||
# Scan
|
||||
c.print("[bold]Running security scan...[/]")
|
||||
scan_source = getattr(bundle, "identifier", "") or getattr(meta, "identifier", "") or identifier
|
||||
if bundle.source == "official":
|
||||
scan_source = "official"
|
||||
else:
|
||||
scan_source = (
|
||||
getattr(bundle, "identifier", "")
|
||||
or getattr(meta, "identifier", "")
|
||||
or identifier
|
||||
)
|
||||
result = scan_skill(q_path, source=scan_source)
|
||||
c.print(format_scan_report(result))
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ def test_do_install_scans_with_resolved_identifier(monkeypatch, tmp_path, hub_en
|
|||
"trust_level": "trusted",
|
||||
"metadata": {},
|
||||
})()
|
||||
|
||||
q_path = tmp_path / "skills" / ".hub" / "quarantine" / "frontend-design"
|
||||
q_path.mkdir(parents=True)
|
||||
(q_path / "SKILL.md").write_text("# Frontend Design")
|
||||
|
|
@ -318,6 +317,60 @@ def test_do_install_scans_with_resolved_identifier(monkeypatch, tmp_path, hub_en
|
|||
assert scanned["source"] == canonical_identifier
|
||||
|
||||
|
||||
def test_do_install_scans_official_bundles_with_source_provenance(
|
||||
monkeypatch, tmp_path, hub_env
|
||||
):
|
||||
import tools.skills_guard as guard
|
||||
import tools.skills_hub as hub
|
||||
|
||||
class _OfficialSource:
|
||||
def inspect(self, identifier):
|
||||
return type("Meta", (), {
|
||||
"extra": {},
|
||||
"identifier": "official/agent/prunus-gaia",
|
||||
})()
|
||||
|
||||
def fetch(self, identifier):
|
||||
return type("Bundle", (), {
|
||||
"name": "prunus-gaia",
|
||||
"files": {"SKILL.md": "# Prunus Gaia"},
|
||||
"source": "official",
|
||||
"identifier": "official/agent/prunus-gaia",
|
||||
"trust_level": "builtin",
|
||||
"metadata": {},
|
||||
})()
|
||||
|
||||
q_path = tmp_path / "skills" / ".hub" / "quarantine" / "prunus-gaia"
|
||||
q_path.mkdir(parents=True)
|
||||
(q_path / "SKILL.md").write_text("# Prunus Gaia")
|
||||
|
||||
scanned = {}
|
||||
|
||||
def _scan_skill(skill_path, source="community"):
|
||||
scanned["source"] = source
|
||||
return guard.ScanResult(
|
||||
skill_name="prunus-gaia",
|
||||
source=source,
|
||||
trust_level="builtin",
|
||||
verdict="safe",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(hub, "ensure_hub_dirs", lambda: None)
|
||||
monkeypatch.setattr(hub, "create_source_router", lambda auth: [_OfficialSource()])
|
||||
monkeypatch.setattr(hub, "quarantine_bundle", lambda bundle: q_path)
|
||||
monkeypatch.setattr(hub, "HubLockFile", lambda: type("Lock", (), {"get_installed": lambda self, name: None})())
|
||||
monkeypatch.setattr(guard, "scan_skill", _scan_skill)
|
||||
monkeypatch.setattr(guard, "format_scan_report", lambda result: "scan ok")
|
||||
monkeypatch.setattr(guard, "should_allow_install", lambda result, force=False: (False, "stop after scan"))
|
||||
|
||||
sink = StringIO()
|
||||
console = Console(file=sink, force_terminal=False, color_system=None)
|
||||
|
||||
do_install("official/agent/prunus-gaia", console=console, skip_confirm=True)
|
||||
|
||||
assert scanned["source"] == "official"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UrlSource-specific install paths: --name override, interactive prompts,
|
||||
# non-interactive error, existing-category scan.
|
||||
|
|
|
|||
|
|
@ -46,15 +46,23 @@ from tools.skills_guard import (
|
|||
|
||||
|
||||
class TestResolveTrustLevel:
|
||||
def test_official_sources_resolve_to_builtin(self):
|
||||
def test_official_source_provenance_resolves_to_builtin(self):
|
||||
assert _resolve_trust_level("official") == "builtin"
|
||||
assert _resolve_trust_level("official/email/agentmail") == "builtin"
|
||||
|
||||
def test_trusted_repos(self):
|
||||
assert _resolve_trust_level("openai/skills") == "trusted"
|
||||
assert _resolve_trust_level("anthropics/skills") == "trusted"
|
||||
assert _resolve_trust_level("openai/skills/some-skill") == "trusted"
|
||||
|
||||
def test_trusted_repo_sibling_prefixes_are_not_trusted(self):
|
||||
assert _resolve_trust_level("openai/skills-evil") == "community"
|
||||
assert _resolve_trust_level("anthropics/skills-foo/frontend-design") == "community"
|
||||
assert _resolve_trust_level("huggingface/skills-bar/some-skill") == "community"
|
||||
|
||||
def test_official_github_namespace_does_not_resolve_to_builtin(self):
|
||||
assert _resolve_trust_level("official/attacker-skill") == "community"
|
||||
assert _resolve_trust_level("official/agent/evil-skill") == "community"
|
||||
|
||||
def test_skills_sh_wrapped_trusted_repos(self):
|
||||
assert _resolve_trust_level("skills-sh/openai/skills/skill-creator") == "trusted"
|
||||
assert _resolve_trust_level("skills-sh/anthropics/skills/frontend-design") == "trusted"
|
||||
|
|
|
|||
|
|
@ -917,12 +917,14 @@ def _resolve_trust_level(source: str) -> str:
|
|||
# Agent-created skills get their own permissive trust level
|
||||
if normalized_source == "agent-created":
|
||||
return "agent-created"
|
||||
# Official optional skills shipped with the repo
|
||||
if normalized_source.startswith("official/") or normalized_source == "official":
|
||||
# Official optional skills must be identified by source provenance, not by
|
||||
# user-controlled GitHub identifiers such as "official/<repo>".
|
||||
if normalized_source == "official":
|
||||
return "builtin"
|
||||
# Check if source matches any trusted repo
|
||||
# Check if source matches any trusted repo exactly, or a skill path inside
|
||||
# that repo. Do not trust sibling repositories that merely share a prefix.
|
||||
for trusted in TRUSTED_REPOS:
|
||||
if normalized_source.startswith(trusted) or normalized_source == trusted:
|
||||
if normalized_source == trusted or normalized_source.startswith(f"{trusted}/"):
|
||||
return "trusted"
|
||||
return "community"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue