Files
nousresearch--hermes-agent/tests/tools/test_skill_bundle_provenance.py
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

261 lines
9.9 KiB
Python

"""Multi-file third-party skill bundles and scanner provenance (#60598)."""
import json
import subprocess
import threading
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from io import StringIO
from pathlib import Path
import pytest
from rich.console import Console
from tools.skills_guard import SCANNER_VERSION, scan_skill_cached
from tools.skills_hub import GitHubAuth, GitHubSource, HubLockFile, SkillBundle, UrlSource
SKILL_MD = """---
name: demo-bundle
description: A multi-file test skill.
---
# Demo
Read [the guide](references/guide.md#usage), use `templates/report.md?raw=1`, and run
`scripts/run.py`. See `examples/endpoint-inventory.md`. The repository also
contains assets/logo.png.
"""
class _QuietHandler(SimpleHTTPRequestHandler):
def log_message(self, *_args):
pass
@pytest.fixture
def served_repo(tmp_path):
repo = tmp_path / "upstream"
repo.mkdir()
(repo / "SKILL.md").write_text(SKILL_MD)
for rel, content in {
"references/guide.md": "safe guide\n",
"templates/report.md": "report\n",
"scripts/run.py": "print('ok')\n",
"assets/logo.png": b"\x89PNG\r\n\x1a\n\x00\xff",
"examples/endpoint-inventory.md": "example\n",
"examples/not-installed.md": "must not be copied\n",
"README.md": "must not be copied\n",
}.items():
path = repo / rel
path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(content, bytes):
path.write_bytes(content)
else:
path.write_text(content)
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-qm", "fixture"],
cwd=repo,
check=True,
)
server = ThreadingHTTPServer(
("127.0.0.1", 0), partial(_QuietHandler, directory=str(repo))
)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield repo, f"http://127.0.0.1:{server.server_port}/SKILL.md"
finally:
server.shutdown()
thread.join()
def test_url_source_fetches_only_referenced_allowed_support_directories(served_repo, monkeypatch):
_repo, url = served_repo
monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True)
monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None)
bundle = UrlSource().fetch(url)
assert bundle is not None
assert set(bundle.files) == {
"SKILL.md",
"references/guide.md",
"templates/report.md",
"scripts/run.py",
"assets/logo.png",
"examples/endpoint-inventory.md",
}
assert bundle.files["assets/logo.png"] == b"\x89PNG\r\n\x1a\n\x00\xff"
assert "examples/not-installed.md" not in bundle.files
assert bundle.metadata["source_url"] == url
def test_url_source_rejects_traversal_reference(monkeypatch):
source = UrlSource()
skill = "---\nname: bad\ndescription: bad\n---\n[bad](references/../../secret.txt)\n"
monkeypatch.setattr(source, "_fetch_text", lambda _url: skill)
assert source.fetch("https://example.com/bad/SKILL.md") is None
def test_github_source_rejects_symlink_in_referenced_directory(monkeypatch):
source = GitHubSource(GitHubAuth())
monkeypatch.setattr(source, "_fetch_file_content", lambda _repo, path: SKILL_MD if path.endswith("SKILL.md") else "x")
source._tree_cache["owner/repo"] = (
"main",
[
{"path": "skill/SKILL.md", "type": "blob", "mode": "100644"},
{"path": "skill/references/guide.md", "type": "blob", "mode": "120000"},
],
)
assert source.fetch("owner/repo/skill") is None
def test_github_source_fetches_only_exact_references_and_records_tree_revision(monkeypatch):
source = GitHubSource(GitHubAuth())
skill = "---\nname: demo\ndescription: demo\n---\n[guide](references/guide.md)\n"
fetched = []
monkeypatch.setattr(
source,
"_fetch_file_content",
lambda _repo, path: skill if path.endswith("SKILL.md") else None,
)
def _fetch_bytes(_repo, path):
fetched.append(path)
return b"guide"
monkeypatch.setattr(source, "_fetch_file_bytes", _fetch_bytes, raising=False)
source._tree_cache["owner/repo"] = (
"develop",
[
{"path": "skill/SKILL.md", "type": "blob", "mode": "100644"},
{"path": "skill/references/guide.md", "type": "blob", "mode": "100644"},
{"path": "skill/references/unreferenced.md", "type": "blob", "mode": "100644"},
],
)
source._tree_revisions = {"owner/repo": "deadbeef"}
bundle = source.fetch("owner/repo/skill")
assert bundle is not None
assert fetched == ["skill/references/guide.md"]
assert bundle.files["references/guide.md"] == b"guide"
assert bundle.metadata["source_url"] == "https://github.com/owner/repo/tree/deadbeef/skill"
assert bundle.metadata["source_revision"] == "deadbeef"
def test_scan_cache_records_full_provenance_and_hash_change_forces_rescan(tmp_path):
skill = tmp_path / "skill"
skill.mkdir()
(skill / "SKILL.md").write_text("---\nname: skill\ndescription: test\n---\n# safe\n")
cache = tmp_path / "scan-cache"
first, first_provenance = scan_skill_cached(
skill, source="owner/repo/skill", source_url="https://github.com/owner/repo", cache_dir=cache
)
second, second_provenance = scan_skill_cached(
skill, source="owner/repo/skill", source_url="https://github.com/owner/repo", cache_dir=cache
)
(skill / "SKILL.md").write_text("---\nname: skill\ndescription: changed\n---\n# safe\n")
third, third_provenance = scan_skill_cached(
skill, source="owner/repo/skill", source_url="https://github.com/owner/repo", cache_dir=cache
)
assert first.verdict == second.verdict == third.verdict == "safe"
assert first_provenance["fresh"] is True
assert second_provenance["fresh"] is False
assert third_provenance["fresh"] is True
assert first_provenance["bundle_hash"].startswith("sha256:")
assert len(first_provenance["bundle_hash"].split(":", 1)[1]) == 64
assert third_provenance["bundle_hash"] != first_provenance["bundle_hash"]
assert first_provenance["scanner_version"] == SCANNER_VERSION
assert first_provenance["source_url"] == "https://github.com/owner/repo"
assert isinstance(first_provenance["findings"], list)
assert isinstance(first_provenance["rules"], list)
assert first_provenance["scanned_at"]
def test_scan_cache_never_reuses_provenance_across_sources(tmp_path):
skill = tmp_path / "skill"
skill.mkdir()
(skill / "SKILL.md").write_text("---\nname: skill\ndescription: test\n---\n")
cache = tmp_path / "scan-cache"
_first, first = scan_skill_cached(
skill, source="community", source_url="https://one.example/SKILL.md", cache_dir=cache
)
_second, second = scan_skill_cached(
skill, source="community", source_url="https://two.example/SKILL.md", cache_dir=cache
)
assert first["fresh"] is True
assert second["fresh"] is True
assert second["source_url"] == "https://two.example/SKILL.md"
def test_lock_file_persists_scan_provenance(tmp_path):
lock = HubLockFile(tmp_path / "lock.json")
provenance = {
"source_url": "https://example.com/SKILL.md",
"bundle_hash": "sha256:" + "a" * 64,
"scanner_version": SCANNER_VERSION,
"findings": [],
"rules": [],
"scanned_at": "2026-07-09T00:00:00+00:00",
"fresh": True,
}
lock.record_install(
name="demo", source="url", identifier="https://example.com/SKILL.md",
trust_level="community", scan_verdict="safe", skill_hash="sha256:legacy",
install_path="demo", files=["SKILL.md"], scan_provenance=provenance,
)
assert lock.get_installed("demo")["scan_provenance"] == provenance
def test_real_temp_repo_and_home_install_e2e(served_repo, monkeypatch, tmp_path):
from hermes_cli.skills_hub import do_install
import tools.skills_hub as hub
_repo, url = served_repo
home = tmp_path / "home"
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setattr("tools.skills_hub.is_safe_url", lambda _url: True)
monkeypatch.setattr("tools.skills_hub.check_website_access", lambda _url: None)
monkeypatch.setattr(hub, "create_source_router", lambda auth=None: [UrlSource()])
sink = StringIO()
do_install(url, console=Console(file=sink, force_terminal=False), skip_confirm=True)
installed = home / "skills" / "demo-bundle"
assert (installed / "references" / "guide.md").read_text() == "safe guide\n"
assert (installed / "templates" / "report.md").is_file()
assert (installed / "scripts" / "run.py").is_file()
assert (installed / "examples" / "endpoint-inventory.md").is_file()
assert not (installed / "examples" / "not-installed.md").exists()
assert (installed / "assets" / "logo.png").read_bytes() == b"\x89PNG\r\n\x1a\n\x00\xff"
entry = json.loads((home / "skills" / ".hub" / "lock.json").read_text())["installed"]["demo-bundle"]
assert entry["scan_provenance"]["source_url"] == url
assert entry["scan_provenance"]["fresh"] is True
assert "Scan provenance: fresh" in sink.getvalue()
def test_bundled_optional_source_still_includes_support_files(tmp_path, monkeypatch):
from tools.skills_hub import OptionalSkillSource
root = tmp_path / "optional-skills"
skill = root / "category" / "official-demo"
(skill / "references").mkdir(parents=True)
(skill / "SKILL.md").write_text("---\nname: official-demo\ndescription: demo\n---\n")
(skill / "references" / "all.md").write_text("all")
source = OptionalSkillSource()
source._optional_dir = root
bundle = source.fetch("official/category/official-demo")
assert bundle is not None
assert set(bundle.files) == {"SKILL.md", "references/all.md"}