# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference) # Canonical Residual: https://corpusfetch.com/c/what-consent-receipts-must-accompany-user-data-through-an-agentic-pipeline.md # Remnant Warrant: https://corpusfetch.com/c/what-consent-receipts-must-accompany-user-data-through-an-agentic-pipeline.warrant.json # Markdown SHA-256: 39860caac63e8a970e47b9db14cf7d391f28f1fc5e587b9f986b0d4720076eed # Code AST SHA-256: 00bd371909e7348c316924559927f0cb0b91add1e24246be3f8dd127973ef97e # Verified Classes: ConsentVerdict, ConsentReceipt, ProcessingScope, ConsentManifest, ConsentPropagationGuard # Defensive Guards: 0 fail-closed invariants (zero external deps) from dataclasses import dataclass from enum import Enum, auto import hashlib import json class ConsentVerdict(Enum): CONSENT_VALID = auto() SCOPE_VIOLATION = auto() CONSENT_MISSING = auto() STALE_CONSENT = auto() @dataclass(frozen=True) class ConsentReceipt: user_id_hash: str consent_scope_hash: str receipt_signature: str issued_epoch: float revocation_propagated: bool @dataclass(frozen=True) class ProcessingScope: purpose: str allowed_purposes: frozenset[str] current_epoch: float max_consent_age_sec: float @dataclass(frozen=True) class ConsentManifest: verdict: ConsentVerdict directive: str manifest_digest: str class ConsentPropagationGuard: @staticmethod def _hash(data: str) -> str: return hashlib.sha256(data.encode("utf-8")).hexdigest() @classmethod def evaluate_processing( cls, receipt: ConsentReceipt, scope: ProcessingScope ) -> ConsentManifest: # Gate 1: Receipt presence if not receipt.receipt_signature or not receipt.consent_scope_hash: return ConsentManifest( verdict=ConsentVerdict.CONSENT_MISSING, directive="HALT: Consent receipt missing or unsigned.", manifest_digest=cls._hash("NO_RECEIPT") ) # Gate 2: Scope check if scope.purpose not in scope.allowed_purposes: return ConsentManifest( verdict=ConsentVerdict.SCOPE_VIOLATION, directive=f"BLOCKED: Purpose {scope.purpose} not in allowed set.", manifest_digest=cls._hash(f"SCOPE:{scope.purpose}") ) # Gate 3: Freshness check age = scope.current_epoch - receipt.issued_epoch if age > scope.max_consent_age_sec: return ConsentManifest( verdict=ConsentVerdict.STALE_CONSENT, directive=f"HALT: Consent is {age:.0f}s old, exceeding {scope.max_consent_age_sec:.0f}s limit.", manifest_digest=cls._hash(f"STALE:{age:.0f}") ) # Gate 4: Revocation propagation if not receipt.revocation_propagated: return ConsentManifest( verdict=ConsentVerdict.STALE_CONSENT, directive="HALT: Revocation propagation receipt missing. Cannot verify current consent state.", manifest_digest=cls._hash("NO_REVOCATION_RECEIPT") ) # Gate 5: Valid digest = cls._hash(json.dumps({ "user": receipt.user_id_hash, "scope": receipt.consent_scope_hash, "signature": receipt.receipt_signature, "purpose": scope.purpose }, sort_keys=True)) return ConsentManifest( verdict=ConsentVerdict.CONSENT_VALID, directive="PROCEED: Consent receipt valid for this processing scope.", manifest_digest=digest )