"""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