Files
hermes-agent/tests/tools/test_clawhub_owner_http.py
T
Teknium 833594cda6 test: opt the ClawHub owner-HTTP fixture into private URLs for the SSRF-guarded stream
The streamed download path now routes through the SSRF-safe client, which
(correctly) refuses the test fixture's 127.0.0.1 registry. Set
HERMES_ALLOW_PRIVATE_URLS for the fixture's lifetime and reset the module
cache on both sides so the guard still fail-closes everywhere else.
2026-09-12 21:50:30 -07:00

102 lines
4.1 KiB
Python

"""Owner-qualified ClawHub fetches retain identity across actual HTTP requests."""
import io
import json
import os
import threading
import zipfile
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
from tools.skills_hub_clawhub import ClawHubSource
from tools.url_safety import _reset_allow_private_cache
@contextmanager
def registry(*, fallback=False, mismatch=False):
requests = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
url = urlsplit(self.path)
query = parse_qs(url.query)
requests.append((url.path, query))
status = 200
if query.get("owner") != ["alice"]:
status, payload = 409, {"code": "AMBIGUOUS_SKILL_SLUG"}
elif url.path.endswith("/download"):
if fallback:
status, payload = 404, {}
else:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as archive:
archive.writestr("SKILL.md", "# Alice fixture")
payload = buf.getvalue()
elif url.path.endswith("/versions"):
payload = {"items": [{"version": "1.0"}]} if fallback else [{"version": "1.0"}]
elif url.path.endswith("/versions/1.0"):
payload = {"files": {"SKILL.md": "# Alice fixture"}}
else:
# Explicit owner must survive even when metadata omits it.
payload = {"skill": {"slug": "collision"}}
if mismatch:
payload["owner"] = {"handle": "bob"}
body = payload if isinstance(payload, bytes) else json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
source = ClawHubSource()
source.BASE_URL = f"http://127.0.0.1:{server.server_port}/api/v1"
# The download path is SSRF-guarded (blocks loopback); opt in for the local fixture server.
prior = os.environ.get("HERMES_ALLOW_PRIVATE_URLS")
os.environ["HERMES_ALLOW_PRIVATE_URLS"] = "true"
_reset_allow_private_cache()
try:
yield source, requests
finally:
if prior is None:
os.environ.pop("HERMES_ALLOW_PRIVATE_URLS", None)
else:
os.environ["HERMES_ALLOW_PRIVATE_URLS"] = prior
_reset_allow_private_cache()
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_qualified_owner_survives_metadata_versions_and_download():
for fallback in (False, True):
with registry(fallback=fallback) as (source, requests):
for identifier in ("@alice/collision", "clawhub/@alice/collision", "alice/skills/collision"):
meta = source.inspect(identifier)
assert meta is not None
assert meta.identifier == "@alice/collision"
bundle = source.fetch(identifier)
assert bundle is not None
assert bundle.identifier == "@alice/collision"
assert bundle.files == {"SKILL.md": "# Alice fixture"}
assert all(query.get("owner") == ["alice"] for _, query in requests)
assert any(path.endswith("/versions") for path, _ in requests)
assert any(path.endswith("/download") for path, _ in requests)
if fallback:
assert any(path.endswith("/versions/1.0") for path, _ in requests)
def test_ambiguous_or_mismatched_owner_never_downloads():
with registry() as (source, requests):
assert source.fetch("collision") is None
assert source.fetch("github-owner/repository/collision") is None
assert len(requests) == 1
with registry(mismatch=True) as (source, requests):
assert source.fetch("@alice/collision") is None
assert len(requests) == 1