# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-provenance-collapse-when-tool-result-tags-survive-prompt-injection-rewriting-in-otel-genai-traces.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-provenance-collapse-when-tool-result-tags-survive-prompt-injection-rewriting-in-otel-genai-traces.warrant.json
# Markdown SHA-256:   d7284c5a9ee3221104717bcc484b011e0d8f7a777b1352752d7398905aa26c0d
# Code AST SHA-256:   02f3900fea04335352227b130ca857bd86532be872734aafccfea365e23744ac
# Verified Classes:   ProvenanceCollapseError, Span
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class ProvenanceCollapseError(RuntimeError):
    """Raised when a tool-result provenance tag no longer binds to the payload
    that a consumer is about to act on."""


@dataclass(frozen=True)
class Span:
    name: str
    attributes: dict
    parent: "Span | None" = None


def _hash(payload: bytes) -> str:
    return hashlib.sha256(payload).hexdigest()


def authorize_tool_result(consuming_span: Span, payload: bytes) -> None:
    """Enforce authorization-grade provenance before acting on tool output.

    Conditions enforced:
      1. Tag originates at the producing span, not inherited across model nodes.
      2. Consumer recomputes the content hash; span hash is advisory only.
      3. Parent chain contains no model-generated content node between the
         tool result span and the consuming span.
    """
    result_span = consuming_span
    while result_span is not None and result_span.attributes.get("gen_ai.tool.name") is None:
        if result_span.attributes.get("gen_ai.operation.name") in ("chat", "summarize"):
            raise ProvenanceCollapseError(
                f"model-generated node {result_span.name!r} sits between tool result "
                f"and consumer {consuming_span.name!r}; provenance tag is inherited, not derived"
            )
        result_span = result_span.parent

    if result_span is None:
        raise ProvenanceCollapseError(
            f"consumer {consuming_span.name!r} has no tool-result ancestor span; "
            f"content is model-generated and must not be authorized under a tool tag"
        )

    recorded = result_span.attributes.get("corpusfetch.provenance.content_sha256")
    if recorded is None:
        raise ProvenanceCollapseError(
            f"tool result span {result_span.name!r} carries no content hash; "
            f"provenance cannot be verified"
        )

    actual = _hash(payload)
    if actual != recorded:
        raise ProvenanceCollapseError(
            f"payload hash {actual[:12]} does not match span-recorded hash "
            f"{recorded[:12]} for tool {result_span.attributes.get('gen_ai.tool.name')!r}; "
            f"content was rewritten after the tool result span was emitted"
        )
