Files
SciMesh/scimesh/sdk/verification.py
T

1414 lines
54 KiB
Python

"""Versioned verifier decisions and core exact/numeric implementations."""
from __future__ import annotations
import json
import hashlib
import hmac
import math
import re
import struct
from decimal import Decimal
from dataclasses import dataclass, field
from enum import Enum
from types import MappingProxyType
from typing import Any, Callable, Iterable, Mapping, Sequence, cast
from ._validation import (
enum_value,
canonical_json,
freeze_json_mapping,
require_exact_keys,
require_identifier,
require_positive_int,
require_sha256,
require_string,
require_task_key,
parse_release,
thaw_json,
)
from .artifacts import OutputManifest, PortSpec
from .identity import ComponentRef, SchemaRef, WorkloadId
from .manifest import TrustMode
class VerificationStatus(str, Enum):
"""The outcome of verification.
Only ``ACCEPTED`` satisfies a stage; ``INCONCLUSIVE`` must never be
treated as success by a reducer default.
"""
ACCEPTED = "accepted"
REJECTED = "rejected"
INCONCLUSIVE = "inconclusive"
@dataclass(frozen=True, slots=True)
class VerificationDecision:
"""An immutable verifier outcome with bounded sanitized evidence.
``accepted_digest`` is set only for accepted decisions; evidence is
limited to 16 KiB and rejects local paths and transport URLs.
"""
status: VerificationStatus
verifier: ComponentRef
reason_code: str
evidence: Mapping[str, Any]
accepted_digest: str | None = None
def __post_init__(self) -> None:
object.__setattr__(
self,
"status",
enum_value(VerificationStatus, self.status, "verification.status"),
)
if not isinstance(self.verifier, ComponentRef):
raise ValueError("verification verifier must be a ComponentRef")
object.__setattr__(
self,
"reason_code",
require_identifier(self.reason_code, "verification.reason_code"),
)
evidence = freeze_json_mapping(
self.evidence, "verification.evidence", forbid_locations=True
)
if (
len(
json.dumps(thaw_json(evidence), sort_keys=True, allow_nan=False).encode(
"utf-8"
)
)
> 16_384
):
raise ValueError("verification evidence exceeds 16 KiB")
object.__setattr__(self, "evidence", evidence)
if self.accepted_digest is not None:
object.__setattr__(
self,
"accepted_digest",
require_sha256(self.accepted_digest, "accepted_digest"),
)
if self.status is VerificationStatus.ACCEPTED and self.accepted_digest is None:
raise ValueError("accepted verification requires an accepted_digest")
if (
self.status is not VerificationStatus.ACCEPTED
and self.accepted_digest is not None
):
raise ValueError("only accepted verification may carry an accepted_digest")
def to_dict(self) -> dict[str, object]:
return {
"status": self.status.value,
"verifier": self.verifier.canonical,
"reason_code": self.reason_code,
"evidence": thaw_json(self.evidence),
"accepted_digest": self.accepted_digest,
}
@classmethod
def from_dict(cls, value: object) -> "VerificationDecision":
if not isinstance(value, Mapping):
raise ValueError("verification decision must be an object")
fields = {"status", "verifier", "reason_code", "evidence", "accepted_digest"}
require_exact_keys(value, fields, "verification decision")
return cls(
status=value["status"], # type: ignore[arg-type]
verifier=ComponentRef.from_dict(value["verifier"]),
reason_code=value["reason_code"], # type: ignore[arg-type]
evidence=value["evidence"], # type: ignore[arg-type]
accepted_digest=value["accepted_digest"], # type: ignore[arg-type]
)
@dataclass(frozen=True, slots=True)
class VerificationBinding:
"""Coordinator-owned scientific identity shared by accepted attempts."""
workload: WorkloadId
task_key: str
package_digest: str
manifest_digest: str
environment_digest: str
parameters_digest: str
input_collection_digest: str
execution_contract_digest: str
selected_features: Mapping[str, str]
optional_fallbacks: Mapping[str, str]
job_id: str
task_id: str
verifier: ComponentRef
sdk_api_version: str
protocol_version: str
manifest_schema_version: int
workflow_schema_version: int
artifact_schemas: tuple[SchemaRef, ...]
trust_mode: TrustMode
def __post_init__(self) -> None:
if not isinstance(self.workload, WorkloadId):
raise ValueError("verification binding workload must be a WorkloadId")
object.__setattr__(
self,
"task_key",
require_task_key(self.task_key, "verification task_key"),
)
object.__setattr__(
self,
"package_digest",
require_sha256(
self.package_digest, "verification package_digest", prefixed=True
),
)
object.__setattr__(
self,
"manifest_digest",
require_sha256(self.manifest_digest, "verification manifest_digest"),
)
object.__setattr__(
self,
"environment_digest",
require_sha256(
self.environment_digest,
"verification environment_digest",
prefixed=True,
),
)
object.__setattr__(
self,
"parameters_digest",
require_sha256(self.parameters_digest, "verification parameters_digest"),
)
object.__setattr__(
self,
"input_collection_digest",
require_sha256(
self.input_collection_digest,
"verification input_collection_digest",
),
)
object.__setattr__(
self,
"execution_contract_digest",
require_sha256(
self.execution_contract_digest,
"verification execution_contract_digest",
),
)
selected_features = freeze_json_mapping(
self.selected_features,
"verification selected_features",
)
optional_fallbacks = freeze_json_mapping(
self.optional_fallbacks,
"verification optional_fallbacks",
)
for name, version in selected_features.items():
require_identifier(name, "verification selected feature")
require_string(
version, "verification selected feature version", max_length=32
)
parse_release(version, "verification selected feature version")
for name, fallback in optional_fallbacks.items():
require_identifier(name, "verification fallback feature")
require_identifier(fallback, "verification fallback")
if set(selected_features).intersection(optional_fallbacks):
raise ValueError("verification feature cannot be selected and fallbacked")
object.__setattr__(self, "selected_features", selected_features)
object.__setattr__(self, "optional_fallbacks", optional_fallbacks)
from ._validation import require_uuid
object.__setattr__(
self, "job_id", require_uuid(self.job_id, "verification job_id")
)
object.__setattr__(
self, "task_id", require_uuid(self.task_id, "verification task_id")
)
if not isinstance(self.verifier, ComponentRef):
raise ValueError("verification binding verifier must be a ComponentRef")
object.__setattr__(
self,
"sdk_api_version",
require_string(
self.sdk_api_version, "verification sdk_api_version", max_length=32
),
)
object.__setattr__(
self,
"protocol_version",
require_string(
self.protocol_version, "verification protocol_version", max_length=32
),
)
parse_release(self.sdk_api_version, "verification sdk_api_version")
parse_release(self.protocol_version, "verification protocol_version")
object.__setattr__(
self,
"manifest_schema_version",
require_positive_int(
self.manifest_schema_version,
"verification manifest_schema_version",
),
)
object.__setattr__(
self,
"workflow_schema_version",
require_positive_int(
self.workflow_schema_version,
"verification workflow_schema_version",
),
)
schemas = tuple(self.artifact_schemas)
if not schemas or any(not isinstance(schema, SchemaRef) for schema in schemas):
raise ValueError(
"verification artifact_schemas must contain schema identities"
)
if len(schemas) != len(set(schemas)) or schemas != tuple(
sorted(schemas, key=lambda schema: schema.canonical)
):
raise ValueError(
"verification artifact_schemas must be unique and canonical"
)
object.__setattr__(self, "artifact_schemas", schemas)
try:
trust_mode = TrustMode(self.trust_mode)
except (TypeError, ValueError) as error:
raise ValueError("verification trust_mode is unsupported") from error
object.__setattr__(self, "trust_mode", trust_mode)
def matches(self, manifest: OutputManifest) -> bool:
if not isinstance(manifest, OutputManifest):
return False
provenance = manifest.provenance
return (
manifest.task_key == self.task_key
and provenance.workload == self.workload
and provenance.package_digest == self.package_digest
and provenance.manifest_digest == self.manifest_digest
and provenance.environment_digest == self.environment_digest
and provenance.parameters_digest == self.parameters_digest
and provenance.input_collection_digest == self.input_collection_digest
and provenance.execution_contract_digest == self.execution_contract_digest
and provenance.selected_features == self.selected_features
and provenance.optional_fallbacks == self.optional_fallbacks
and provenance.job_id == self.job_id
and provenance.task_id == self.task_id
and provenance.verifier == self.verifier
and provenance.sdk_api_version == self.sdk_api_version
and provenance.protocol_version == self.protocol_version
and provenance.manifest_schema_version == self.manifest_schema_version
and provenance.workflow_schema_version == self.workflow_schema_version
and provenance.artifact_schemas == self.artifact_schemas
and provenance.trust_mode == self.trust_mode.value
)
def to_dict(self) -> dict[str, object]:
return {
"workload": self.workload.to_dict(),
"task_key": self.task_key,
"package_digest": self.package_digest,
"manifest_digest": self.manifest_digest,
"environment_digest": self.environment_digest,
"parameters_digest": self.parameters_digest,
"input_collection_digest": self.input_collection_digest,
"execution_contract_digest": self.execution_contract_digest,
"selected_features": thaw_json(self.selected_features),
"optional_fallbacks": thaw_json(self.optional_fallbacks),
"job_id": self.job_id,
"task_id": self.task_id,
"verifier": self.verifier.canonical,
"sdk_api_version": self.sdk_api_version,
"protocol_version": self.protocol_version,
"manifest_schema_version": self.manifest_schema_version,
"workflow_schema_version": self.workflow_schema_version,
"artifact_schemas": [schema.canonical for schema in self.artifact_schemas],
"trust_mode": self.trust_mode.value,
}
@classmethod
def from_dict(cls, value: object) -> "VerificationBinding":
if not isinstance(value, Mapping):
raise ValueError("verification binding must be an object")
fields = {
"workload",
"task_key",
"package_digest",
"manifest_digest",
"environment_digest",
"parameters_digest",
"input_collection_digest",
"execution_contract_digest",
"selected_features",
"optional_fallbacks",
"job_id",
"task_id",
"verifier",
"sdk_api_version",
"protocol_version",
"manifest_schema_version",
"workflow_schema_version",
"artifact_schemas",
"trust_mode",
}
require_exact_keys(value, fields, "verification binding")
schemas = value["artifact_schemas"]
if not isinstance(schemas, list):
raise ValueError("verification artifact_schemas must be an array")
return cls(
workload=WorkloadId.from_dict(value["workload"]),
task_key=value["task_key"], # type: ignore[arg-type]
package_digest=value["package_digest"], # type: ignore[arg-type]
manifest_digest=value["manifest_digest"], # type: ignore[arg-type]
environment_digest=value["environment_digest"], # type: ignore[arg-type]
parameters_digest=value["parameters_digest"], # type: ignore[arg-type]
input_collection_digest=value["input_collection_digest"], # type: ignore[arg-type]
execution_contract_digest=value["execution_contract_digest"], # type: ignore[arg-type]
selected_features=value["selected_features"], # type: ignore[arg-type]
optional_fallbacks=value["optional_fallbacks"], # type: ignore[arg-type]
job_id=value["job_id"], # type: ignore[arg-type]
task_id=value["task_id"], # type: ignore[arg-type]
verifier=ComponentRef.from_dict(value["verifier"]),
sdk_api_version=value["sdk_api_version"], # type: ignore[arg-type]
protocol_version=value["protocol_version"], # type: ignore[arg-type]
manifest_schema_version=value["manifest_schema_version"], # type: ignore[arg-type]
workflow_schema_version=value["workflow_schema_version"], # type: ignore[arg-type]
artifact_schemas=tuple(SchemaRef.from_dict(item) for item in schemas),
trust_mode=value["trust_mode"], # type: ignore[arg-type]
)
@dataclass(frozen=True, slots=True)
class VerifyContext:
"""Coordinator-owned verification inputs: expected outputs, byte budget,
quorum size, an optional reference, and the binding for non-trusted modes.
Multi-vote contexts automatically require distinct authenticated owners.
"""
expected_outputs: Mapping[str, PortSpec]
max_output_bytes: int
minimum_matches: int = 1
reference: OutputManifest | None = None
require_distinct_owners: bool = False
binding: VerificationBinding | None = None
trust_mode: TrustMode = TrustMode.TRUSTED
def __post_init__(self) -> None:
if not isinstance(self.expected_outputs, Mapping) or not self.expected_outputs:
raise ValueError("expected_outputs must be a non-empty object")
ports: dict[str, PortSpec] = {}
for name, port in self.expected_outputs.items():
canonical = require_identifier(name, "expected output port")
if not isinstance(port, PortSpec):
raise ValueError("expected_outputs values must be PortSpec values")
ports[canonical] = port
object.__setattr__(self, "expected_outputs", MappingProxyType(ports))
object.__setattr__(
self,
"max_output_bytes",
require_positive_int(self.max_output_bytes, "max_output_bytes"),
)
object.__setattr__(
self,
"minimum_matches",
require_positive_int(self.minimum_matches, "minimum_matches"),
)
if not isinstance(self.require_distinct_owners, bool):
raise ValueError("require_distinct_owners must be a boolean")
# A multi-vote quorum is never allowed to fall back to anonymous
# candidate counting. Single-candidate trusted verification remains
# convenient, while every quorum must carry coordinator-owned owners.
if self.minimum_matches > 1:
object.__setattr__(self, "require_distinct_owners", True)
if self.binding is not None and not isinstance(
self.binding, VerificationBinding
):
raise ValueError("binding must be a VerificationBinding")
try:
trust_mode = TrustMode(self.trust_mode)
except (TypeError, ValueError) as error:
raise ValueError("verification trust_mode is unsupported") from error
object.__setattr__(self, "trust_mode", trust_mode)
if self.binding is not None and self.binding.trust_mode is not trust_mode:
raise ValueError(
"verification context trust mode does not match its binding"
)
if trust_mode is not TrustMode.TRUSTED and self.binding is None:
raise ValueError("non-trusted verification requires a coordinator binding")
if trust_mode is TrustMode.UNTRUSTED_QUORUM:
if self.minimum_matches < 2:
raise ValueError(
"untrusted quorum requires at least two matching owners"
)
object.__setattr__(self, "require_distinct_owners", True)
if self.require_distinct_owners and self.binding is None:
raise ValueError("multi-owner verification requires a coordinator binding")
if self.reference is not None:
if not isinstance(self.reference, OutputManifest):
raise ValueError("reference must be an OutputManifest")
self.reference.validate_against(
self.expected_outputs, max_output_bytes=self.max_output_bytes
)
if self.binding is not None and not self.binding.matches(self.reference):
raise ValueError(
"reference output does not match the coordinator binding"
)
_CANDIDATE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$")
def _candidate_identity(value: object, field: str) -> str:
text = require_string(value, field, max_length=128)
if not _CANDIDATE_ID_PATTERN.fullmatch(text):
raise ValueError(f"{field} must be an opaque coordinator identity")
return text
@dataclass(frozen=True, slots=True)
class CandidateOutput:
"""Coordinator-authenticated identity envelope for one output attempt.
The SDK validates the envelope shape, but the coordinator is responsible
for constructing it from authenticated attempt and owner records. Worker
payloads must never be allowed to choose these identity fields directly.
``owner_id`` may be omitted only for a trusted single-candidate path.
"""
candidate_id: str
owner_id: str | None
manifest: OutputManifest
authentication_tag: str | None = None
_coordinator_authenticated: bool = field(default=False, init=False, repr=False)
def __post_init__(self) -> None:
object.__setattr__(
self,
"candidate_id",
_candidate_identity(self.candidate_id, "candidate_id"),
)
if self.owner_id is not None:
object.__setattr__(
self,
"owner_id",
_candidate_identity(self.owner_id, "owner_id"),
)
if not isinstance(self.manifest, OutputManifest):
raise ValueError("candidate manifest must be an OutputManifest")
if self.authentication_tag is not None:
if self.owner_id is None:
raise ValueError("authenticated candidate requires an owner_id")
object.__setattr__(
self,
"authentication_tag",
require_sha256(self.authentication_tag, "candidate authentication_tag"),
)
def _authentication_payload(self) -> bytes:
return canonical_json(
{
"candidate_id": self.candidate_id,
"owner_id": self.owner_id,
"manifest": self.manifest.to_dict(),
}
).encode("utf-8")
def authenticated_by(self, key: bytes) -> bool:
if (
self.authentication_tag is None
or not isinstance(key, bytes)
or len(key) < 32
):
return False
expected = hmac.new(
key, self._authentication_payload(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(self.authentication_tag, expected)
@classmethod
def from_coordinator_record(
cls,
candidate_id: str,
owner_id: str,
manifest: OutputManifest,
authentication_key: bytes,
) -> "CandidateOutput":
"""Create an envelope from authenticated coordinator-owned records.
Worker wire payloads must use :meth:`from_dict`, which deliberately
cannot confer this process-local authority marker.
"""
if not isinstance(authentication_key, bytes) or len(authentication_key) < 32:
raise ValueError(
"candidate authentication key must contain at least 32 bytes"
)
unsigned = cls(candidate_id, owner_id, manifest)
tag = hmac.new(
authentication_key,
unsigned._authentication_payload(),
hashlib.sha256,
).hexdigest()
value = cls(candidate_id, owner_id, manifest, tag)
object.__setattr__(value, "_coordinator_authenticated", True)
return value
@property
def coordinator_authenticated(self) -> bool:
return self._coordinator_authenticated
def to_dict(self) -> dict[str, object]:
return {
"candidate_id": self.candidate_id,
"owner_id": self.owner_id,
"manifest": self.manifest.to_dict(),
"authentication_tag": self.authentication_tag,
}
@classmethod
def from_dict(cls, value: object) -> "CandidateOutput":
if not isinstance(value, Mapping):
raise ValueError("candidate output must be an object")
require_exact_keys(
value,
{"candidate_id", "owner_id", "manifest", "authentication_tag"},
"candidate output",
)
return cls(
candidate_id=value["candidate_id"], # type: ignore[arg-type]
owner_id=value["owner_id"], # type: ignore[arg-type]
manifest=OutputManifest.from_dict(value["manifest"]),
authentication_tag=value["authentication_tag"], # type: ignore[arg-type]
)
@dataclass(frozen=True, slots=True, init=False)
class CandidateOutputs:
"""Immutable candidate set with replay-safe coordinator identities.
For source compatibility, ``CandidateOutputs((manifest,))`` creates one
anonymous synthetic envelope for trusted local verification. Raw manifests
cannot be combined or used to form a quorum; distributed callers provide
explicit :class:`CandidateOutput` envelopes instead.
"""
candidates: tuple[CandidateOutput, ...]
def __init__(
self,
manifests: Sequence[CandidateOutput | OutputManifest] = (),
*,
candidates: Sequence[CandidateOutput | OutputManifest] | None = None,
) -> None:
if candidates is not None:
if manifests:
raise ValueError("provide candidate values only once")
manifests = candidates
values = tuple(manifests)
raw = tuple(value for value in values if isinstance(value, OutputManifest))
if raw:
if len(values) != 1:
raise ValueError(
"raw OutputManifest compatibility is limited to one trusted candidate"
)
manifest = raw[0]
normalized = (
CandidateOutput(
candidate_id=f"trusted-{manifest.manifest_digest}",
owner_id=None,
manifest=manifest,
),
)
else:
if any(not isinstance(value, CandidateOutput) for value in values):
raise ValueError("candidates must contain CandidateOutput values")
normalized = cast(tuple[CandidateOutput, ...], values)
candidate_ids = [value.candidate_id for value in normalized]
if len(candidate_ids) != len(set(candidate_ids)):
raise ValueError("candidate_id values must be unique")
object.__setattr__(self, "candidates", normalized)
@property
def manifests(self) -> tuple[OutputManifest, ...]:
"""Compatibility view for trusted code that only needs manifests."""
return tuple(candidate.manifest for candidate in self.candidates)
def to_dict(self) -> dict[str, object]:
return {"candidates": [candidate.to_dict() for candidate in self.candidates]}
@classmethod
def from_dict(cls, value: object) -> "CandidateOutputs":
if not isinstance(value, Mapping):
raise ValueError("candidate outputs must be an object")
require_exact_keys(value, {"candidates"}, "candidate outputs")
candidates = value["candidates"]
if not isinstance(candidates, list):
raise ValueError("candidate outputs candidates must be an array")
return cls(
candidates=tuple(CandidateOutput.from_dict(item) for item in candidates)
)
@classmethod
def from_authenticated_dict(
cls,
value: object,
authentication_key: bytes,
) -> "CandidateOutputs":
"""Decode an internal coordinator envelope and verify every MAC.
The signing key remains in the SDK-owned transport adapter and is never
placed in :class:`VerifyContext` or exposed to package verifier code.
"""
decoded = cls.from_dict(value)
if not isinstance(authentication_key, bytes) or len(authentication_key) < 32:
raise ValueError(
"candidate authentication key must contain at least 32 bytes"
)
for candidate in decoded.candidates:
if not candidate.authenticated_by(authentication_key):
raise ValueError("candidate envelope authentication failed")
object.__setattr__(candidate, "_coordinator_authenticated", True)
return decoded
def _authentication_failure(
context: VerifyContext,
candidates: CandidateOutputs,
identity: ComponentRef,
) -> VerificationDecision | None:
if (
context.trust_mode is TrustMode.TRUSTED
and context.minimum_matches == 1
and not context.require_distinct_owners
):
return None
invalid = sum(
candidate.owner_id is None or not candidate.coordinator_authenticated
for candidate in candidates.candidates
)
if invalid:
return VerificationDecision(
VerificationStatus.REJECTED,
identity,
"coordinator-authentication-required",
{
"candidate_count": len(candidates.candidates),
"unauthenticated_count": invalid,
},
)
return None
def _verify_loaded_candidates(
context: VerifyContext,
candidates: CandidateOutputs,
identity: ComponentRef,
compare: Callable[[OutputManifest, OutputManifest], VerificationDecision],
) -> VerificationDecision:
"""Apply a package-owned loader/comparator without trusting vote replay."""
if not isinstance(context, VerifyContext) or not isinstance(
candidates, CandidateOutputs
):
raise ValueError("verifier requires VerifyContext and CandidateOutputs")
authentication_failure = _authentication_failure(context, candidates, identity)
if authentication_failure is not None:
return authentication_failure
if context.reference is None:
return VerificationDecision(
VerificationStatus.INCONCLUSIVE,
identity,
"reference-required",
{"candidate_count": len(candidates.candidates)},
)
if context.require_distinct_owners and any(
candidate.owner_id is None for candidate in candidates.candidates
):
return VerificationDecision(
VerificationStatus.REJECTED,
identity,
"owner-identity-required",
{"candidate_count": len(candidates.candidates)},
)
matched = 0
compared = 0
invalid = 0
seen_owners: set[str] = set()
for candidate in sorted(candidates.candidates, key=lambda item: item.candidate_id):
if candidate.owner_id is not None:
if candidate.owner_id in seen_owners:
continue
seen_owners.add(candidate.owner_id)
try:
if context.binding is not None and not context.binding.matches(
candidate.manifest
):
raise ValueError("candidate does not match the coordinator binding")
candidate.manifest.validate_against(
context.expected_outputs,
max_output_bytes=context.max_output_bytes,
)
decision = compare(context.reference, candidate.manifest)
except Exception:
invalid += 1
continue
compared += 1
if decision.status is VerificationStatus.ACCEPTED:
matched += 1
evidence = {
"matched": matched,
"required": context.minimum_matches,
"compared": compared,
"invalid_count": invalid,
}
if matched >= context.minimum_matches:
return VerificationDecision(
VerificationStatus.ACCEPTED,
identity,
"reference-match",
evidence,
context.reference.digest,
)
if compared >= context.minimum_matches:
return VerificationDecision(
VerificationStatus.REJECTED,
identity,
"reference-mismatch",
evidence,
)
return VerificationDecision(
VerificationStatus.INCONCLUSIVE,
identity,
"insufficient-evidence",
evidence,
)
class ExactArtifactVerifier:
"""Whole-artifact SHA-256 acceptance for byte-exact workloads.
Compares logical port/collection/schema/content digests while ignoring
coordinator UUIDs, timestamps, and worker identity; counts at most one
vote per owner and accepts only a declared reference match or an
unambiguous quorum.
"""
identity = ComponentRef("exact-artifact", 1)
configuration: Mapping[str, object] = MappingProxyType({})
def verify(
self,
context: VerifyContext,
candidates: CandidateOutputs,
) -> VerificationDecision:
if not isinstance(context, VerifyContext) or not isinstance(
candidates, CandidateOutputs
):
raise ValueError(
"exact verifier requires VerifyContext and CandidateOutputs"
)
authentication_failure = _authentication_failure(
context, candidates, self.identity
)
if authentication_failure is not None:
return authentication_failure
if context.require_distinct_owners:
missing_owner_count = sum(
candidate.owner_id is None for candidate in candidates.candidates
)
if missing_owner_count:
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"owner-identity-required",
{
"candidate_count": len(candidates.candidates),
"missing_owner_count": missing_owner_count,
},
)
valid: list[CandidateOutput] = []
invalid = 0
for candidate in candidates.candidates:
try:
if context.binding is not None and not context.binding.matches(
candidate.manifest
):
raise ValueError("candidate does not match the coordinator binding")
candidate.manifest.validate_against(
context.expected_outputs,
max_output_bytes=context.max_output_bytes,
)
except ValueError:
invalid += 1
else:
valid.append(candidate)
if not valid:
return VerificationDecision(
VerificationStatus.REJECTED
if candidates.candidates
else VerificationStatus.INCONCLUSIVE,
self.identity,
"no-valid-candidates" if candidates.candidates else "no-candidates",
{
"candidate_count": len(candidates.candidates),
"invalid_count": invalid,
},
)
owner_digests: dict[str, set[str]] = {}
for candidate in valid:
if candidate.owner_id is not None:
owner_digests.setdefault(candidate.owner_id, set()).add(
candidate.manifest.digest
)
equivocating_owner_count = sum(
len(digests) > 1 for digests in owner_digests.values()
)
if equivocating_owner_count:
# Do not echo owner IDs or conflicting digests into durable
# evidence. Counts are sufficient for policy and audit routing.
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"owner-equivocation",
{
"candidate_count": len(candidates.candidates),
"equivocating_owner_count": equivocating_owner_count,
},
)
counts: dict[str, int] = {}
seen_owners: set[str] = set()
duplicate_owner_candidates = 0
for candidate in sorted(valid, key=lambda value: value.candidate_id):
if candidate.owner_id is not None:
if candidate.owner_id in seen_owners:
duplicate_owner_candidates += 1
continue
seen_owners.add(candidate.owner_id)
digest = candidate.manifest.digest
counts[digest] = counts.get(digest, 0) + 1
def with_duplicate_evidence(evidence: dict[str, int]) -> dict[str, int]:
if duplicate_owner_candidates:
evidence["duplicate_owner_candidates"] = duplicate_owner_candidates
return evidence
if context.reference is not None:
expected_digest = context.reference.digest
matched = counts.get(expected_digest, 0)
if matched >= context.minimum_matches:
return VerificationDecision(
VerificationStatus.ACCEPTED,
self.identity,
"reference-match",
with_duplicate_evidence(
{
"matched": matched,
"required": context.minimum_matches,
"invalid_count": invalid,
}
),
expected_digest,
)
status = (
VerificationStatus.REJECTED
if sum(counts.values()) >= context.minimum_matches
else VerificationStatus.INCONCLUSIVE
)
return VerificationDecision(
status,
self.identity,
"reference-mismatch"
if status is VerificationStatus.REJECTED
else "insufficient-evidence",
with_duplicate_evidence(
{
"matched": matched,
"required": context.minimum_matches,
"invalid_count": invalid,
}
),
)
ordered = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
digest, matches = ordered[0]
tied = len(ordered) > 1 and ordered[1][1] == matches
if matches >= context.minimum_matches and not tied:
return VerificationDecision(
VerificationStatus.ACCEPTED,
self.identity,
"quorum-match",
with_duplicate_evidence(
{
"matched": matches,
"required": context.minimum_matches,
"distinct_digests": len(counts),
"invalid_count": invalid,
}
),
digest,
)
if tied and matches >= context.minimum_matches:
status = VerificationStatus.REJECTED
reason = "conflicting-quorums"
else:
status = VerificationStatus.INCONCLUSIVE
reason = "insufficient-evidence"
return VerificationDecision(
status,
self.identity,
reason,
with_duplicate_evidence(
{
"largest_group": matches,
"required": context.minimum_matches,
"distinct_digests": len(counts),
"invalid_count": invalid,
}
),
)
def _float_ulp_distance(left: float, right: float) -> int:
"""Return IEEE-754 binary64 representable steps between two finite values."""
sign = 0x8000000000000000
magnitude = sign - 1
def ordered(value: float) -> int:
bits = struct.unpack(">Q", struct.pack(">d", value))[0]
return sign - (bits & magnitude) if bits & sign else sign + bits
return abs(ordered(left) - ordered(right))
def _numeric_digest_value(value: object, depth: int = 0) -> object:
if depth > 64:
raise ValueError("numeric value nesting exceeds 64 levels")
if isinstance(value, Mapping):
if any(not isinstance(key, str) for key in value):
raise ValueError("numeric objects must use JSON string keys")
return {
key: _numeric_digest_value(child, depth + 1) for key, child in value.items()
}
if isinstance(value, (list, tuple)):
return [_numeric_digest_value(child, depth + 1) for child in value]
if isinstance(value, float) and math.isnan(value):
return {"$number": "nan"}
return value
def _numeric_value_within_limit(value: object, max_elements: int) -> bool:
"""Bound a loaded numeric shape before comparison or digest allocation."""
pending = [value]
visited = 0
while pending:
current = pending.pop()
visited += 1
if visited > max_elements:
return False
if isinstance(current, Mapping):
if len(current) > max_elements - visited:
return False
pending.extend(current.values())
elif isinstance(current, (list, tuple)):
if len(current) > max_elements - visited:
return False
pending.extend(current)
return True
def _decimal_evidence(value: Decimal) -> int | float | str:
"""Return bounded, JSON-safe numeric evidence without range exceptions."""
if value == value.to_integral_value() and abs(value).adjusted() <= 18:
return int(value)
try:
converted = float(value)
except (OverflowError, ValueError):
converted = math.inf
if math.isfinite(converted):
return converted
text = format(value, ".16E")
return text if len(text) <= 64 else text[:64]
@dataclass(frozen=True, slots=True)
class NumericTolerance:
"""Bounded numeric comparison policy: absolute/relative/ULP tolerances,
NaN policy, and a maximum element count for structured values."""
absolute: float = 0.0
relative: float = 0.0
max_ulps: int = 0
nan_policy: str = "reject"
max_elements: int = 1_000_000
def __post_init__(self) -> None:
for field in ("absolute", "relative"):
value = getattr(self, field)
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or value < 0
):
raise ValueError(
f"numeric tolerance {field} must be a finite non-negative number"
)
try:
converted = float(value)
except (OverflowError, ValueError) as error:
raise ValueError(
f"numeric tolerance {field} must be a finite non-negative number"
) from error
if not math.isfinite(converted):
raise ValueError(
f"numeric tolerance {field} must be a finite non-negative number"
)
object.__setattr__(self, field, converted)
if (
isinstance(self.max_ulps, bool)
or not isinstance(self.max_ulps, int)
or self.max_ulps < 0
):
raise ValueError(
"numeric tolerance max_ulps must be a non-negative integer"
)
if self.nan_policy not in {"reject", "equal"}:
raise ValueError("numeric tolerance nan_policy must be reject or equal")
object.__setattr__(
self,
"max_elements",
require_positive_int(self.max_elements, "numeric tolerance max_elements"),
)
class NumericToleranceVerifier:
"""Reference-based structured numeric comparison verifier.
Requires a package-owned ``value_loader`` to turn artifacts into bounded
structured values; without one, verification returns ``inconclusive``
rather than accepting bytes it did not parse.
"""
identity = ComponentRef("numeric-tolerance", 1)
def __init__(
self,
tolerance: NumericTolerance,
value_loader: Callable[[OutputManifest], object] | None = None,
) -> None:
if not isinstance(tolerance, NumericTolerance):
raise ValueError("tolerance must be NumericTolerance")
if value_loader is not None and not callable(value_loader):
raise ValueError("value_loader must be callable")
self.tolerance = tolerance
self._value_loader = value_loader
@property
def configuration(self) -> Mapping[str, object]:
return MappingProxyType(
{
"absolute": self.tolerance.absolute,
"relative": self.tolerance.relative,
"max_ulps": self.tolerance.max_ulps,
"nan_policy": self.tolerance.nan_policy,
"max_elements": self.tolerance.max_elements,
}
)
def verify(
self,
context: VerifyContext,
candidates: CandidateOutputs,
) -> VerificationDecision:
if not isinstance(context, VerifyContext) or not isinstance(
candidates, CandidateOutputs
):
raise ValueError(
"numeric verifier requires VerifyContext and CandidateOutputs"
)
if self._value_loader is None:
return VerificationDecision(
VerificationStatus.INCONCLUSIVE,
self.identity,
"loader-unavailable",
{"candidate_count": len(candidates.candidates)},
)
def compare(
reference: OutputManifest, candidate: OutputManifest
) -> VerificationDecision:
assert self._value_loader is not None
return self.verify_values(
self._value_loader(reference),
self._value_loader(candidate),
)
return _verify_loaded_candidates(context, candidates, self.identity, compare)
def verify_values(self, expected: object, actual: object) -> VerificationDecision:
if not _numeric_value_within_limit(
expected,
self.tolerance.max_elements,
) or not _numeric_value_within_limit(actual, self.tolerance.max_elements):
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"element-limit",
{"max_elements": self.tolerance.max_elements},
)
mismatch = self._compare(expected, actual, "$", 0)
if mismatch is None:
digest = hashlib.sha256(
canonical_json(_numeric_digest_value(actual)).encode("utf-8")
).hexdigest()
return VerificationDecision(
VerificationStatus.ACCEPTED,
self.identity,
"within-tolerance",
{
"absolute": self.tolerance.absolute,
"relative": self.tolerance.relative,
"max_ulps": self.tolerance.max_ulps,
},
digest,
)
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
mismatch[0],
{"location": mismatch[1], **mismatch[2]},
)
def _compare(
self,
expected: object,
actual: object,
location: str,
depth: int,
) -> tuple[str, str, dict[str, object]] | None:
if depth > 64:
return "nesting-limit", location, {}
if isinstance(expected, bool) or isinstance(actual, bool):
return None if expected is actual else ("value-mismatch", location, {})
if isinstance(expected, (int, float)) and isinstance(actual, (int, float)):
if isinstance(expected, int) and isinstance(actual, int):
if max(abs(expected).bit_length(), abs(actual).bit_length()) > 1024:
return "numeric-range", location, {}
difference_int = abs(actual - expected)
allowed_decimal = max(
Decimal(str(self.tolerance.absolute)),
Decimal(str(self.tolerance.relative))
* Decimal(max(abs(expected), abs(actual))),
)
if Decimal(difference_int) <= allowed_decimal:
return None
return (
"numeric-mismatch",
location,
{
"absolute_error": difference_int,
"allowed_error": _decimal_evidence(allowed_decimal),
"ulp_distance": 0,
},
)
if (
isinstance(expected, int)
and abs(expected).bit_length() > 1024
or isinstance(actual, int)
and abs(actual).bit_length() > 1024
):
return "numeric-range", location, {}
left = float(expected) if isinstance(expected, int) else expected
right = float(actual) if isinstance(actual, int) else actual
if math.isnan(left) or math.isnan(right):
if (
self.tolerance.nan_policy == "equal"
and math.isnan(left)
and math.isnan(right)
):
return None
return "nan-policy", location, {}
if not math.isfinite(left) or not math.isfinite(right):
return "non-finite", location, {}
expected_decimal = (
Decimal(expected)
if isinstance(expected, int)
else Decimal.from_float(expected)
)
actual_decimal = (
Decimal(actual)
if isinstance(actual, int)
else Decimal.from_float(actual)
)
difference_decimal = abs(actual_decimal - expected_decimal)
allowed_decimal = max(
Decimal(str(self.tolerance.absolute)),
Decimal(str(self.tolerance.relative))
* max(abs(expected_decimal), abs(actual_decimal)),
)
ulp_distance = (
_float_ulp_distance(expected, actual)
if isinstance(expected, float) and isinstance(actual, float)
else None
)
if difference_decimal <= allowed_decimal or (
ulp_distance is not None and ulp_distance <= self.tolerance.max_ulps
):
return None
return (
"numeric-mismatch",
location,
{
"absolute_error": _decimal_evidence(difference_decimal),
"allowed_error": _decimal_evidence(allowed_decimal),
"ulp_distance": ulp_distance if ulp_distance is not None else 0,
},
)
if isinstance(expected, Mapping) and isinstance(actual, Mapping):
if any(not isinstance(key, str) for key in expected) or any(
not isinstance(key, str) for key in actual
):
return "type-mismatch", location, {}
if set(expected) != set(actual):
return (
"shape-mismatch",
location,
{
"missing_keys": sorted(
str(key) for key in set(expected) - set(actual)
)[:32],
"extra_keys": sorted(
str(key) for key in set(actual) - set(expected)
)[:32],
},
)
for key in sorted(expected, key=str):
mismatch = self._compare(
expected[key], actual[key], f"{location}.{key}", depth + 1
)
if mismatch is not None:
return mismatch
return None
if isinstance(expected, (list, tuple)) and isinstance(actual, (list, tuple)):
if len(expected) != len(actual):
return (
"shape-mismatch",
location,
{"expected_length": len(expected), "actual_length": len(actual)},
)
for index, (left, right) in enumerate(zip(expected, actual)):
mismatch = self._compare(left, right, f"{location}[{index}]", depth + 1)
if mismatch is not None:
return mismatch
return None
if type(expected) is not type(actual):
return (
"type-mismatch",
location,
{
"expected_type": type(expected).__name__,
"actual_type": type(actual).__name__,
},
)
return None if expected == actual else ("value-mismatch", location, {})
class CanonicalRecordVerifier:
"""Package-owned record canonicalizer with bounded core comparison logic."""
identity = ComponentRef("canonical-record", 1)
def __init__(
self,
canonicalizer: Callable[[object], bytes],
*,
max_records: int = 1_000_000,
record_loader: Callable[[OutputManifest], Iterable[object]] | None = None,
) -> None:
if not callable(canonicalizer):
raise ValueError("canonicalizer must be callable")
self._canonicalizer = canonicalizer
self._max_records = require_positive_int(max_records, "max_records")
if record_loader is not None and not callable(record_loader):
raise ValueError("record_loader must be callable")
self._record_loader = record_loader
@property
def configuration(self) -> Mapping[str, object]:
return MappingProxyType({"max_records": self._max_records})
def verify(
self,
context: VerifyContext,
candidates: CandidateOutputs,
) -> VerificationDecision:
if not isinstance(context, VerifyContext) or not isinstance(
candidates, CandidateOutputs
):
raise ValueError(
"canonical verifier requires VerifyContext and CandidateOutputs"
)
if self._record_loader is None:
return VerificationDecision(
VerificationStatus.INCONCLUSIVE,
self.identity,
"loader-unavailable",
{"candidate_count": len(candidates.candidates)},
)
def compare(
reference: OutputManifest, candidate: OutputManifest
) -> VerificationDecision:
assert self._record_loader is not None
return self.verify_records(
self._record_loader(reference),
self._record_loader(candidate),
)
return _verify_loaded_candidates(context, candidates, self.identity, compare)
def verify_records(
self, expected: Iterable[object], actual: Iterable[object]
) -> VerificationDecision:
expected_digest = hashlib.sha256()
actual_digest = hashlib.sha256()
counts = [0, 0]
try:
for index, (stream, digest) in enumerate(
((expected, expected_digest), (actual, actual_digest))
):
for record in stream:
counts[index] += 1
if counts[index] > self._max_records:
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"record-limit",
{"max_records": self._max_records},
)
encoded = self._canonicalizer(record)
if not isinstance(encoded, bytes):
raise ValueError("canonicalizer must return bytes")
digest.update(len(encoded).to_bytes(8, "big"))
digest.update(encoded)
except Exception:
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"canonicalization-failed",
{},
)
if counts[0] != counts[1] or expected_digest.digest() != actual_digest.digest():
return VerificationDecision(
VerificationStatus.REJECTED,
self.identity,
"canonical-mismatch",
{"expected_records": counts[0], "actual_records": counts[1]},
)
digest = expected_digest.hexdigest()
return VerificationDecision(
VerificationStatus.ACCEPTED,
self.identity,
"canonical-match",
{"records": counts[0]},
digest,
)