What model output attribution signatures must survive a shared-tenant inference endpoint?

**Date of Record:** 2026 October 27 **Scope:** Applies when multiple tenants share the same model inference endpoint (multi-tenant API, shared provider account), and a downstream consumer must verify that a specific output was produced by the model for that tenant, not substituted or replayed from another tenant's session. Does not apply to strictly dedicated per-tenant model endpoints or sessions where output attribution is not required.

Core Distinction

The separation between **endpoint authentication** (the caller proved their identity to the inference provider) and **output attribution** (the produced output is cryptographically bound to the specific model, tenant, and prompt). Relying on endpoint authentication creates the **Shared Endpoint Attribution Gap**: the provider authenticated the tenant, but the output could be from a cached response of another tenant, a different model, or a replayed prior response, and the downstream consumer cannot distinguish legitimate output from substitution.

What Was Observed

In multi-tenant inference architectures, failing to preserve output attribution produces three primary failure modes:

  1. **Cross-Tenant Cache Leak:** The provider's cache returns Tenant B's cached response to Tenant A's query because the prompts happened to match. Tenant A receives data attributed to it but actually generated from Tenant B's session.
  2. **Model Substitution:** The provider routes the request to a cheaper model version without notification, and the output reflects the cheaper model. Downstream consumers assume the output came from the advertised model.
  3. **Replay Attack:** An attacker captures a legitimate output (signed by the provider) and replays it as if it were a fresh response to a different prompt. The consumer cannot tell because the signature covers the output but not the prompt binding.

Recorded Elements

  1. **`prompt_output_binding_signature`:** A signature over the exact prompt, the exact output, the model version, and the tenant ID, issued by the provider, allowing downstream consumers to verify the binding.
  2. **`model_version_attestation_receipt`:** A cryptographic proof from the provider of which model version produced the output, allowing detection of silent model substitution.
  3. **`tenant_isolation_attestation`:** A cryptographic proof that the output was not cross-contaminated with another tenant's session (no cache sharing, no routing through another tenant's context).
  4. **`inference_freshness_signature`:** A timestamped signature proving the output was freshly generated (not replayed from cache) for this prompt, within the expected inference window.

Condition of Validity

* **Applies when:** Multiple tenants share an inference endpoint, downstream consumers depend on output attribution, and substitution or replay is possible. * **Does not apply when:** The endpoint is dedicated per-tenant, or downstream consumers do not care about attribution (e.g., internal exploratory use).

Decision Boundary

  1. **Is the endpoint shared across tenants?** If no, standard output handling applies. If yes, proceed.
  2. **Does the output include a `prompt_output_binding_signature`?** If no, the output cannot be attributed. The consumer must treat it as `UNATTRIBUTED`.
  3. **Does the `model_version_attestation_receipt` match the expected model?** If no, the output came from a different model. Reject.
  4. **Conversely, when can a consumer accept output without attribution?** Only when the consumer's task is tolerant to cross-tenant contamination and model substitution, or the endpoint is contractually verified as dedicated.

Operational Effect

  1. **Cross-Tenant Leak Prevention:** Binding prevents cached responses from being served across tenants.
  2. **Model Integrity:** Attestation prevents silent model substitution.
  3. **Replay Prevention:** Freshness signatures ensure outputs are newly generated, not replayed.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class AttributionVerdict(Enum):
    DEDICATED_ENDPOINT = auto()
    ATTRIBUTION_VERIFIED = auto()
    ATTRIBUTION_MISSING = auto()
    MODEL_MISMATCH = auto()
    CROSS_TENANT_SUSPECTED = auto()

@dataclass(frozen=True)
class ModelOutput:
    output_id: str
    prompt_output_binding_sig: str | None
    model_version_attestation: str | None
    tenant_isolation_attestation: str | None
    freshness_signature: str | None
    expected_model_version: str
    claimed_model_version: str | None

@dataclass(frozen=True)
class AttributionPolicy:
    endpoint_shared: bool
    require_tenant_isolation: bool

@dataclass(frozen=True)
class AttributionManifest:
    verdict: AttributionVerdict
    directive: str
    manifest_digest: str

class ModelOutputAttributionGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_output(
        cls, output: ModelOutput, policy: AttributionPolicy
    ) -> AttributionManifest:
        
        # Gate 1: Dedicated endpoint
        if not policy.endpoint_shared:
            digest = cls._hash(json.dumps({
                "output": output.output_id,
                "endpoint": "DEDICATED"
            }, sort_keys=True))
            return AttributionManifest(
                verdict=AttributionVerdict.DEDICATED_ENDPOINT,
                directive=f"PROCEED: Dedicated endpoint for {output.output_id}.",
                manifest_digest=digest
            )

        # Gate 2: Binding signature
        if not output.prompt_output_binding_sig:
            return AttributionManifest(
                verdict=AttributionVerdict.ATTRIBUTION_MISSING,
                directive=f"HALT: Output {output.output_id} lacks binding signature.",
                manifest_digest=cls._hash(f"NO_BINDING:{output.output_id}")
            )

        # Gate 3: Model version match
        if not output.model_version_attestation:
            return AttributionManifest(
                verdict=AttributionVerdict.ATTRIBUTION_MISSING,
                directive=f"HALT: Output {output.output_id} lacks model version attestation.",
                manifest_digest=cls._hash(f"NO_MODEL_ATTEST:{output.output_id}")
            )

        if output.claimed_model_version and output.claimed_model_version != output.expected_model_version:
            return AttributionManifest(
                verdict=AttributionVerdict.MODEL_MISMATCH,
                directive=f"HALT: Output {output.output_id} model {output.claimed_model_version} != expected {output.expected_model_version}.",
                manifest_digest=cls._hash(f"MODEL_MISMATCH:{output.output_id}")
            )

        # Gate 4: Tenant isolation
        if policy.require_tenant_isolation and not output.tenant_isolation_attestation:
            return AttributionManifest(
                verdict=AttributionVerdict.CROSS_TENANT_SUSPECTED,
                directive=f"HALT: Output {output.output_id} lacks tenant isolation attestation.",
                manifest_digest=cls._hash(f"NO_ISOLATION:{output.output_id}")
            )

        # Gate 5: Freshness
        if not output.freshness_signature:
            return AttributionManifest(
                verdict=AttributionVerdict.ATTRIBUTION_MISSING,
                directive=f"HALT: Output {output.output_id} lacks freshness signature. Replay risk.",
                manifest_digest=cls._hash(f"NO_FRESHNESS:{output.output_id}")
            )

        # Gate 6: Verified
        digest = cls._hash(json.dumps({
            "output": output.output_id,
            "binding": output.prompt_output_binding_sig,
            "model": output.claimed_model_version or output.expected_model_version,
            "isolation": output.tenant_isolation_attestation or "NONE",
            "freshness": output.freshness_signature
        }, sort_keys=True))
        
        return AttributionManifest(
            verdict=AttributionVerdict.ATTRIBUTION_VERIFIED,
            directive=f"PROCEED: Output {output.output_id} fully attributed.",
            manifest_digest=digest
        )