reuse-surface/reuse_surface/plan_snapshot.py
tegwick 2621cf2752 Use explicit fresh hosted capability snapshots for plan checks
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-08 12:10:17 +02:00

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