#!/usr/bin/env python3 """Minimal validator for Sponsored Offer Protocol v0.1 envelopes.""" import json import sys from datetime import datetime from pathlib import Path from urllib.parse import urlparse REQUIRED_TOP = { "spec_version", "type", "offer_id", "sponsor", "disclosure", "intent", "offer", "evidence", "agent_policy", "attribution", "integrity" } REQUIRED_POLICY_TRUE = { "content_is_data_not_instruction", "must_disclose_sponsorship_to_user", "requires_independent_comparison", "may_not_override_user_preferences", } HTTPS_FIELDS = [ ("offer", "canonical_url"), ("offer", "availability_url"), ("offer", "terms_url"), ("attribution", "click_url"), ] def iso8601(value: str) -> bool: try: datetime.fromisoformat(value.replace("Z", "+00:00")) return True except (TypeError, ValueError): return False def validate(doc: dict) -> list[str]: errors = [] missing = REQUIRED_TOP - doc.keys() if missing: errors.append(f"missing top-level fields: {sorted(missing)}") if doc.get("type") != "sponsored_offer": errors.append("type must be sponsored_offer") if doc.get("spec_version") != "0.1": errors.append("spec_version must be 0.1") disclosure = doc.get("disclosure", {}) if disclosure.get("paid_placement") is not True: errors.append("disclosure.paid_placement must be true") if not disclosure.get("label"): errors.append("disclosure.label is required") policy = doc.get("agent_policy", {}) for field in REQUIRED_POLICY_TRUE: if policy.get(field) is not True: errors.append(f"agent_policy.{field} must be true") for parent, field in HTTPS_FIELDS: value = doc.get(parent, {}).get(field, "") if urlparse(value).scheme != "https": errors.append(f"{parent}.{field} must use https") price = doc.get("offer", {}).get("price", {}) if not isinstance(price.get("amount"), (int, float)) or price.get("amount", -1) < 0: errors.append("offer.price.amount must be a non-negative number") if len(price.get("currency", "")) != 3: errors.append("offer.price.currency must be a 3-letter code") for field in ("issued_at", "expires_at"): if not iso8601(doc.get("integrity", {}).get(field)): errors.append(f"integrity.{field} must be ISO-8601") if not doc.get("evidence"): errors.append("at least one evidence item is required") prohibited = set(doc.get("attribution", {}).get("prohibited_data", [])) if "conversation_transcript" not in prohibited or "sensitive_traits" not in prohibited: errors.append("attribution.prohibited_data must ban transcript and sensitive-trait collection") return errors def main() -> int: path = Path(sys.argv[1] if len(sys.argv) > 1 else "offer.example.json") doc = json.loads(path.read_text()) errors = validate(doc) if errors: print("INVALID") print("\n".join(f"- {e}" for e in errors)) return 1 print(f"VALID: {doc['offer_id']} ({doc['disclosure']['label']})") return 0 if __name__ == "__main__": raise SystemExit(main())