Source code for aeat.application.ledger._evidence_input

"""In-memory evidence-input representation for on-host LLM reading.

A transient, in-process container for the decrypted bytes of a transaction's
attached evidence (a purchase invoice or a linked attachment), read from secure
storage so an on-host reader -- the in-tree text-layer or a local vision model --
can consume it.

CRITICAL (``sensitive-financial-data-secure-storage-only``): this object holds
decrypted ``FINANCIAL`` bytes in process memory ONLY. It MUST NEVER be persisted,
serialized to disk, written to a temp file, embedded in a persisted document, or
logged. It carries no JSON serializer; ``EvidenceInput.model_dump`` and
``EvidenceInput.model_dump_json`` are overridden to raise so a stray
persistence call fails loudly rather than leaking bytes out of secure storage. The
raw ``data`` field is excluded from ``repr`` for the same reason.
"""

from __future__ import annotations

from typing import Never, Self, SupportsIndex, override

from pydantic import BaseModel, Field, model_serializer, model_validator

from ...core import STRICT_FROZEN_CONFIG
from ...core.config import Settings
from ...core.external_constants import PDF_MIME_TYPE
from ...core.hashing import sha256_hex
from ...domain.attachments import AttachmentStoreProtocol
from ._evidence import MediaKind, PurchaseInvoiceEvidence, PurchaseInvoiceEvidenceInputError

__all__ = [
    "EvidenceInput",
    "cloud_evidence_read_permitted",
    "resolve_attachment_evidence_input",
    "resolve_purchase_invoice_evidence_input",
]


[docs] def cloud_evidence_read_permitted(settings: Settings, *, acknowledged: bool) -> bool: """Whether an off-host cloud evidence read is permitted for THIS invocation. On-host reading is always allowed and is the default; this gate governs only the cloud exception. A cloud read is permitted only when the ``cloud_evidence_upload`` capability resolves enabled for the active profile AND the operator acknowledged the upload for this specific invocation. The capability resolution (``resolve_active_capability``) is the single place the posture is computed: the gestor-mode bar is applied first and absolutely (``aeat_evidence_gestor_mode``), then the active profile's opt-in/out fact, then — when no profile fact is set — the global ``aeat_evidence_cloud_upload_permitted`` flag as the fallback default (so existing deployments behave unchanged until a profile sets the capability). A capability can only NARROW this floor, never widen it. The acknowledgement is never sticky -- it must be re-affirmed each time (sensitive-financial-data-secure-storage-only). Args: settings: Resolved deployment settings carrying the consent posture. acknowledged: Whether the operator acknowledged the off-host upload for this invocation. Returns: ``True`` only when the capability resolves enabled AND ``acknowledged``. """ from ...core import ServiceCapability from ..user_profile import resolve_active_capability decision = resolve_active_capability(ServiceCapability.CLOUD_EVIDENCE_UPLOAD, settings=settings) if not decision.enabled: return False return acknowledged
_REFUSAL_MESSAGE = ( "EvidenceInput must never be serialized, iterated, or persisted; it holds decrypted " "FINANCIAL bytes in memory only (sensitive-financial-data-secure-storage-only)." )
[docs] class EvidenceInput(BaseModel): """Transient in-memory evidence bytes plus the provenance needed to cite them. Built by the resolver from evidence already held in secure storage. The bytes live only for the duration of a single on-host read; this model is never persisted (see the module docstring and the overridden dump methods). Attributes: media_kind: Whether the bytes are a PDF or an image. mime_type: Concrete MIME type of the bytes (e.g. ``application/pdf``). data: The decrypted evidence bytes, in memory only. Excluded from ``repr``. content_sha256: 64-character lowercase hex SHA-256 of :attr:`data`; the content address the bytes were read under. Enforced to match ``data``. evidence_id: Originating purchase-invoice ``evidence_id`` when the bytes came from a :class:`PurchaseInvoiceEvidence` record, else ``None``. attachment_id: Originating ``attachment_id`` when the bytes came from an :class:`~aeat.domain.attachments.Attachment`, else ``None``. """ model_config = STRICT_FROZEN_CONFIG media_kind: MediaKind mime_type: str = Field(min_length=1) data: bytes = Field(repr=False) content_sha256: str = Field(min_length=64, max_length=64) evidence_id: str | None = None attachment_id: str | None = None @model_validator(mode="after") def _verify_content_address(self) -> Self: """Reject bytes whose digest does not match the declared content address.""" if not self.data: raise ValueError("EvidenceInput.data must not be empty") digest = sha256_hex(self.data) if digest != self.content_sha256: raise ValueError("EvidenceInput.content_sha256 must equal sha256(data)") if self.evidence_id is None and self.attachment_id is None: raise ValueError("EvidenceInput must carry an evidence_id or an attachment_id for provenance") return self @model_serializer def _refuse_model_serialization(self) -> dict[str, object]: """Refuse pydantic serialization on every route, including nested dumps. Registered as the model serializer so a parent model that embeds an ``EvidenceInput`` and calls ``model_dump`` / ``model_dump_json`` also raises here, rather than serializing the raw ``data`` bytes through the field schema. """ raise NotImplementedError(_REFUSAL_MESSAGE) @override def model_dump(self, *args: object, **kwargs: object) -> Never: # pyright: ignore[reportIncompatibleMethodOverride] # reason: deliberate persistence tripwire """Refuse serialization -- decrypted FINANCIAL bytes must never be persisted.""" raise NotImplementedError(_REFUSAL_MESSAGE) @override def model_dump_json(self, *args: object, **kwargs: object) -> Never: # pyright: ignore[reportIncompatibleMethodOverride] # reason: deliberate persistence tripwire """Refuse JSON serialization -- decrypted FINANCIAL bytes must never be persisted.""" raise NotImplementedError(_REFUSAL_MESSAGE) @override def __iter__(self) -> Never: # pyright: ignore[reportIncompatibleMethodOverride] # reason: deliberate persistence tripwire """Refuse iteration / ``dict()`` -- it would expose the raw ``data`` bytes.""" raise NotImplementedError(_REFUSAL_MESSAGE) @override def __reduce_ex__(self, protocol: SupportsIndex) -> Never: """Refuse pickling -- it would embed the raw ``data`` bytes.""" raise NotImplementedError(_REFUSAL_MESSAGE)
def _media_kind_from_mime(mime_type: str) -> MediaKind: """Map a stored attachment MIME type to the reader's ``MediaKind``.""" if mime_type == PDF_MIME_TYPE: return MediaKind.PDF if mime_type.startswith("image/"): return MediaKind.IMAGE raise PurchaseInvoiceEvidenceInputError( f"evidence media type {mime_type!r} cannot be read; only PDF and image evidence is supported", suggestion="aeat app ledger evidence list", )
[docs] def resolve_attachment_evidence_input(attachment_id: str, *, store: AttachmentStoreProtocol) -> EvidenceInput: """Read a linked attachment's bytes from secure storage into an ``EvidenceInput``. Loads the attachment manifest and its encrypted blob from the :class:`~aeat.domain.attachments.AttachmentStoreProtocol` (active bucket) into memory. No file is written. Args: attachment_id: Content-addressed id of the linked attachment. store: Attachment store bound to the operation's secure-storage bucket. Returns: :class:`EvidenceInput`: In-memory bytes plus provenance, for an on-host read. """ manifest = store.load_manifest(attachment_id) data = store.read_bytes(manifest.sha256) return EvidenceInput( media_kind=_media_kind_from_mime(manifest.mime_type), mime_type=manifest.mime_type, data=data, content_sha256=manifest.sha256, attachment_id=manifest.attachment_id, )
[docs] def resolve_purchase_invoice_evidence_input( evidence: PurchaseInvoiceEvidence, *, store: AttachmentStoreProtocol, ) -> EvidenceInput: """Read a purchase-invoice evidence record's bytes from secure storage. Reads via the record's ``attachment_id`` -- the in-store byte home written at ``add`` time. A record without an ``attachment_id`` predates that contract and has no in-store bytes to read; this raises rather than falling back to the cleartext ``source_path`` (sensitive-financial-data-secure-storage-only). Args: evidence: The purchase-invoice evidence record. store: Attachment store bound to the operation's secure-storage bucket. Returns: :class:`EvidenceInput`: In-memory bytes plus provenance, for an on-host read. Raises: PurchaseInvoiceEvidenceInputError: When the record carries no ``attachment_id``. """ if evidence.attachment_id is None: raise PurchaseInvoiceEvidenceInputError( f"evidence {evidence.evidence_id!r} has no in-store attachment; its bytes are not in secure storage", suggestion="re-add the evidence so its bytes are stored in the encrypted attachment store", ) manifest = store.load_manifest(evidence.attachment_id) data = store.read_bytes(manifest.sha256) return EvidenceInput( media_kind=_media_kind_from_mime(manifest.mime_type), mime_type=manifest.mime_type, data=data, content_sha256=manifest.sha256, evidence_id=evidence.evidence_id, attachment_id=manifest.attachment_id, )