mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
181 lines
6.7 KiB
Python
181 lines
6.7 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, monkeypatch):
|
|
# The fixture intentionally serves over loopback. Keep exercising the real
|
|
# HTTP transport while opting this test server into private-address access.
|
|
monkeypatch.setattr("tools.url_safety._global_allow_private_urls", lambda: True)
|
|
|
|
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_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"}
|