fix: retain ClawHub owner through version and bundle requests

This commit is contained in:
Teknium
2026-09-07 02:14:50 -07:00
parent f986a2b103
commit 76de6ec5a8
3 changed files with 116 additions and 88 deletions
+90
View File
@@ -0,0 +1,90 @@
"""Owner-qualified ClawHub fetches retain identity across actual HTTP requests."""
import io
import json
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
@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 = [{"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"
try:
yield source, requests
finally:
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
-78
View File
@@ -431,84 +431,6 @@ class TestClawHubSource(unittest.TestCase):
self.assertIsNone(meta)
mock_get.assert_called_once()
@patch("tools.skills_hub.httpx.get")
def test_inspect_passes_owner_param_to_disambiguate_slug(self, mock_get):
"""An @owner/slug identifier must reach the detail API as ?owner=.
ClawHub answers a slug claimed by multiple owners with 409
AMBIGUOUS_SKILL_SLUG on the bare detail GET (#104117); the disambiguated
form only resolves when the owner hint is forwarded as a query param."""
mock_get.return_value = _MockResponse(
status_code=200,
json_data={
"slug": "ponytail",
"displayName": "ponytail",
"summary": "Lazy senior dev mode",
"owner": {"handle": "dietrichgebert"},
"latestVersion": {"version": "1.0.0"},
},
)
meta = self.src.inspect("clawhub/@dietrichgebert/ponytail")
self.assertIsNotNone(meta)
self.assertEqual(meta.extra.get("owner"), "dietrichgebert")
args, kwargs = mock_get.call_args
self.assertTrue(args[0].endswith("/skills/ponytail"))
self.assertEqual(kwargs["params"], {"owner": "dietrichgebert"})
@patch("tools.skills_hub.httpx.get")
def test_inspect_bare_slug_omits_owner_param(self, mock_get):
"""Bare slugs keep the plain detail GET — no owner to forward."""
mock_get.return_value = _MockResponse(
status_code=200,
json_data={
"slug": "caldav-calendar",
"displayName": "CalDAV Calendar",
"summary": "Calendar integration",
"latestVersion": {"version": "1.0.0"},
},
)
meta = self.src.inspect("caldav-calendar")
self.assertIsNotNone(meta)
_, kwargs = mock_get.call_args
self.assertIsNone(kwargs.get("params"))
@patch("tools.skills_hub._ssrf_safe_http_get")
@patch("tools.skills_hub.httpx.get")
def test_fetch_qualified_slug_resolves_ambiguous_slug_end_to_end(self, mock_get, mock_safe_get):
"""install clawhub/@owner/slug must succeed for a multi-owner slug:
the detail GET carries ?owner=, so ClawHub never answers 409."""
def side_effect(url, *args, **kwargs):
if url.endswith("/skills/ponytail"):
return _MockResponse(
status_code=200,
json_data={
"slug": "ponytail",
"owner": {"handle": "dietrichgebert"},
"latestVersion": {"version": "1.0.0"},
},
)
if url.endswith("/skills/ponytail/versions/1.0.0"):
return _MockResponse(
status_code=200,
json_data={"files": {"SKILL.md": "# Skill"}},
)
return _MockResponse(status_code=404, json_data={})
mock_get.side_effect = side_effect
mock_safe_get.return_value = _MockResponse(status_code=200, text="# Skill")
bundle = self.src.fetch("@dietrichgebert/ponytail")
self.assertIsNotNone(bundle)
self.assertEqual(bundle.name, "ponytail")
self.assertEqual(bundle.files["SKILL.md"], "# Skill")
detail_args, detail_kwargs = mock_get.call_args_list[0]
self.assertTrue(detail_args[0].endswith("/skills/ponytail"))
self.assertEqual(detail_kwargs["params"], {"owner": "dietrichgebert"})
class TestClawHubCatalogWalkBounded(unittest.TestCase):