Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
109 lines
4.8 KiB
Python
109 lines
4.8 KiB
Python
"""Read-only hosted discovery with explicit, bounded source acceptance."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from datetime import date, datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
MAX_SNAPSHOT_BYTES = 5 * 1024 * 1024
|
|
|
|
|
|
class SnapshotUnavailable(ValueError):
|
|
"""The selected source cannot support a planning recommendation."""
|
|
|
|
|
|
def load_hosted_snapshot(
|
|
base_url: str, *, max_age_hours: float = 24
|
|
) -> tuple[list[dict[str, Any]], str, dict[str, Any]]:
|
|
"""Fetch the public index; never send intent, tokens or mutation requests.
|
|
|
|
Acceptance concerns the hub's composition and warning signals. It does not
|
|
certify that every upstream repository has published its latest capability.
|
|
Failure must not become a local fallback or a 'new capability' verdict.
|
|
"""
|
|
url = base_url.rstrip("/") + "/v1/federated"
|
|
try:
|
|
parsed = urllib.parse.urlsplit(base_url)
|
|
valid_url = (parsed.scheme in {"http", "https"} and parsed.hostname
|
|
and not parsed.username and not parsed.password
|
|
and not parsed.query and not parsed.fragment)
|
|
except ValueError:
|
|
valid_url = False
|
|
if not valid_url:
|
|
raise SnapshotUnavailable("hub URL must be HTTP(S), without credentials, query or fragment")
|
|
if not math.isfinite(max_age_hours) or max_age_hours <= 0:
|
|
raise SnapshotUnavailable("maximum index age must be a positive finite number of hours")
|
|
|
|
request = urllib.request.Request(
|
|
url, headers={"Accept": "application/json", "User-Agent": "reuse-surface/0.1"}
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=30) as response:
|
|
if response.status != 200:
|
|
raise SnapshotUnavailable(f"hosted index returned HTTP {response.status}")
|
|
if response.headers.get("X-Federation-Warnings"):
|
|
raise SnapshotUnavailable("hosted index reports federation warnings; inspect its sources")
|
|
raw = response.read(MAX_SNAPSHOT_BYTES + 1)
|
|
except urllib.error.HTTPError as exc:
|
|
raise SnapshotUnavailable(f"hosted index returned HTTP {exc.code}") from exc
|
|
except (urllib.error.URLError, OSError, ValueError) as exc:
|
|
if isinstance(exc, SnapshotUnavailable):
|
|
raise
|
|
raise SnapshotUnavailable("hosted index could not be fetched; no local fallback used") from exc
|
|
if len(raw) > MAX_SNAPSHOT_BYTES:
|
|
raise SnapshotUnavailable("hosted index exceeds the 5 MiB response limit")
|
|
try:
|
|
payload = json.loads(raw)
|
|
except (ValueError, UnicodeError) as exc:
|
|
raise SnapshotUnavailable("hosted index is not valid JSON") from exc
|
|
if not isinstance(payload, dict) or payload.get("stale") is not False:
|
|
raise SnapshotUnavailable("hosted index is stale or its freshness signal is missing")
|
|
|
|
now = datetime.now(timezone.utc)
|
|
try:
|
|
composed = datetime.fromisoformat(payload["composed_at"])
|
|
updated = payload["updated"]
|
|
date.fromisoformat(updated)
|
|
if composed.tzinfo is None:
|
|
raise ValueError("timezone missing")
|
|
except (KeyError, TypeError, ValueError) as exc:
|
|
raise SnapshotUnavailable("hosted index has invalid composition metadata") from exc
|
|
age = (now - composed).total_seconds()
|
|
if age < -timedelta(minutes=5).total_seconds() or age > max_age_hours * 3600:
|
|
raise SnapshotUnavailable("hosted index composition is outside the accepted age window")
|
|
|
|
capabilities = payload.get("capabilities")
|
|
if not isinstance(capabilities, list) or not capabilities:
|
|
raise SnapshotUnavailable("hosted index has no usable capability collection")
|
|
ids: set[str] = set()
|
|
for item in capabilities:
|
|
if (not isinstance(item, dict)
|
|
or not isinstance(item.get("id"), str) or not item["id"].strip()
|
|
or item["id"] in ids
|
|
or not isinstance(item.get("name"), str)
|
|
or not isinstance(item.get("summary"), str)
|
|
or not isinstance(item.get("tags", []), list)
|
|
or any(not isinstance(tag, str) for tag in item.get("tags", []))
|
|
or any(item.get(key) is not None and not isinstance(item[key], str)
|
|
for key in ("owner", "vector"))):
|
|
raise SnapshotUnavailable("hosted index contains malformed or duplicate capabilities")
|
|
ids.add(item["id"])
|
|
|
|
provenance = {
|
|
"kind": "hosted",
|
|
"url": url,
|
|
"fetched_at": now.isoformat(),
|
|
"composed_at": payload["composed_at"],
|
|
"capability_count": len(capabilities),
|
|
"sha256": hashlib.sha256(raw).hexdigest(),
|
|
"digest_format": "response-bytes",
|
|
"max_age_hours": max_age_hours,
|
|
"stale": False,
|
|
"federation_warnings": False,
|
|
}
|
|
return capabilities, updated, provenance
|