833594cda6
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.
102 lines
4.1 KiB
Python
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
|