sable-sphynx 0.7.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- sphynx/workflows.py +792 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Generation-pinned, streaming GCS staging for one registered binary model.
|
|
2
|
+
|
|
3
|
+
This path is separate from the source/capture/report JSON store and its limits.
|
|
4
|
+
Registration and approval pins are trusted operator inputs, never PR fields.
|
|
5
|
+
No uploads, URL fetching, candidate execution or model loading are implemented.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import hashlib
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from types import MappingProxyType
|
|
16
|
+
|
|
17
|
+
from . import model_artifacts as models
|
|
18
|
+
from .contracts import HostedError
|
|
19
|
+
|
|
20
|
+
REGISTRATION_SCHEMA = "sphynx.hosted.registered-model-artifact.v1"
|
|
21
|
+
CHUNK_BYTES = 1024 * 1024
|
|
22
|
+
MAX_TOTAL_BYTES = 128 * 1024 * 1024
|
|
23
|
+
STAGING_TIMEOUT_SECONDS = 180
|
|
24
|
+
REQUEST_TIMEOUT_SECONDS = 15
|
|
25
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
26
|
+
_LOCATION = re.compile(r"gs://([a-z0-9][a-z0-9._-]{1,221}[a-z0-9])/([A-Za-z0-9_./-]+)#([1-9][0-9]{0,19})\Z")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _require(condition, code="invalid_registered_model"):
|
|
30
|
+
if not condition:
|
|
31
|
+
raise HostedError(code, "registered model receipt or staged bytes are invalid")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def registered_model_prefix(manifest_digest):
|
|
35
|
+
_require(type(manifest_digest) is str and _DIGEST.fullmatch(manifest_digest))
|
|
36
|
+
return f"prepared/models/{models.MODEL_ID}/{models.REVISION}/{manifest_digest[7:]}/"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ModelObjectReceipt:
|
|
41
|
+
location: str
|
|
42
|
+
digest: str
|
|
43
|
+
size_bytes: int
|
|
44
|
+
bucket: str
|
|
45
|
+
object_name: str
|
|
46
|
+
generation: int
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, value):
|
|
50
|
+
_require(type(value) is dict and set(value) == {"location", "digest", "size_bytes"})
|
|
51
|
+
location, digest, size = value["location"], value["digest"], value["size_bytes"]
|
|
52
|
+
match = _LOCATION.fullmatch(location) if type(location) is str and len(location) <= 2048 else None
|
|
53
|
+
_require(match is not None and not any(part in ("", ".", "..") for part in match.group(2).split("/")))
|
|
54
|
+
_require(type(digest) is str and _DIGEST.fullmatch(digest)
|
|
55
|
+
and type(size) is int and 0 < size <= models.MAX_FILE_BYTES)
|
|
56
|
+
return cls(location, digest, size, match.group(1), match.group(2), int(match.group(3)))
|
|
57
|
+
|
|
58
|
+
def to_dict(self):
|
|
59
|
+
return {"location": self.location, "digest": self.digest, "size_bytes": self.size_bytes}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ModelRegistration:
|
|
64
|
+
model_ref: str
|
|
65
|
+
manifest_digest: str
|
|
66
|
+
object_prefix: str
|
|
67
|
+
files: tuple[tuple[str, ModelObjectReceipt], ...]
|
|
68
|
+
bucket: str
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, value, *, trusted_bucket, approved_manifest_pins):
|
|
72
|
+
_require(type(trusted_bucket) is str and bool(re.fullmatch(r"[a-z0-9][a-z0-9._-]{1,221}[a-z0-9]", trusted_bucket)))
|
|
73
|
+
_require(isinstance(approved_manifest_pins, (dict, MappingProxyType))
|
|
74
|
+
and set(approved_manifest_pins) == {models.MODEL_REF}, "unapproved_model_manifest")
|
|
75
|
+
_require(type(value) is dict and set(value) == {"schema_version", "model_ref", "manifest_digest", "object_prefix", "files"})
|
|
76
|
+
_require(value["schema_version"] == REGISTRATION_SCHEMA and value["model_ref"] == models.MODEL_REF)
|
|
77
|
+
inventory = models.model_artifact_inventory()
|
|
78
|
+
expected_pin = inventory["manifest.json"]["digest"]
|
|
79
|
+
_require(approved_manifest_pins[models.MODEL_REF] == expected_pin
|
|
80
|
+
and value["manifest_digest"] == expected_pin, "unapproved_model_manifest")
|
|
81
|
+
prefix = registered_model_prefix(expected_pin)
|
|
82
|
+
_require(value["object_prefix"] == prefix, "unregistered_model_object")
|
|
83
|
+
_require(type(value["files"]) is dict and set(value["files"]) == set(inventory), "model_closure_mismatch")
|
|
84
|
+
receipts = []
|
|
85
|
+
for relative, expected in sorted(inventory.items()):
|
|
86
|
+
receipt = ModelObjectReceipt.from_dict(value["files"][relative])
|
|
87
|
+
_require(receipt.bucket == trusted_bucket and receipt.object_name == prefix + relative, "unregistered_model_object")
|
|
88
|
+
_require(receipt.digest == expected["digest"] and receipt.size_bytes == expected["size_bytes"], "model_receipt_identity_mismatch")
|
|
89
|
+
receipts.append((relative, receipt))
|
|
90
|
+
_require(sum(receipt.size_bytes for _, receipt in receipts) <= MAX_TOTAL_BYTES)
|
|
91
|
+
return cls(models.MODEL_REF, expected_pin, prefix, tuple(receipts), trusted_bucket)
|
|
92
|
+
|
|
93
|
+
def to_dict(self):
|
|
94
|
+
return {"schema_version": REGISTRATION_SCHEMA, "model_ref": self.model_ref,
|
|
95
|
+
"manifest_digest": self.manifest_digest, "object_prefix": self.object_prefix,
|
|
96
|
+
"files": {name: receipt.to_dict() for name, receipt in self.files}}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class RegisteredModelStager:
|
|
100
|
+
"""Read only exact registered generations using a trusted platform GCS client."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, registration, *, trusted_bucket, approved_manifest_pins, client=None):
|
|
103
|
+
self.registration = ModelRegistration.from_dict(registration, trusted_bucket=trusted_bucket,
|
|
104
|
+
approved_manifest_pins=approved_manifest_pins)
|
|
105
|
+
self._provided_client = client
|
|
106
|
+
|
|
107
|
+
def _client(self):
|
|
108
|
+
if self._provided_client is None:
|
|
109
|
+
try:
|
|
110
|
+
from google.cloud import storage
|
|
111
|
+
self._provided_client = storage.Client()
|
|
112
|
+
except Exception:
|
|
113
|
+
raise HostedError("model_store_unavailable", "trusted model storage client is unavailable") from None
|
|
114
|
+
return self._provided_client
|
|
115
|
+
|
|
116
|
+
def _stream(self, receipt, destination, deadline):
|
|
117
|
+
"""Bounded reads and writes; a large weight is never parsed as JSON."""
|
|
118
|
+
temporary = destination.with_name(destination.name + ".part")
|
|
119
|
+
try:
|
|
120
|
+
blob = self._client().bucket(receipt.bucket).blob(receipt.object_name, generation=receipt.generation)
|
|
121
|
+
_require(time.monotonic() < deadline, "model_staging_timeout")
|
|
122
|
+
blob.reload(if_generation_match=receipt.generation, timeout=REQUEST_TIMEOUT_SECONDS, retry=None)
|
|
123
|
+
_require(type(blob.size) is int and blob.size == receipt.size_bytes
|
|
124
|
+
and int(blob.generation) == receipt.generation, "model_generation_metadata_mismatch")
|
|
125
|
+
digest, observed = hashlib.sha256(), 0
|
|
126
|
+
with blob.open("rb", chunk_size=CHUNK_BYTES, if_generation_match=receipt.generation,
|
|
127
|
+
timeout=REQUEST_TIMEOUT_SECONDS, retry=None) as stream:
|
|
128
|
+
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
129
|
+
with os.fdopen(descriptor, "wb") as target:
|
|
130
|
+
while True:
|
|
131
|
+
_require(time.monotonic() < deadline, "model_staging_timeout")
|
|
132
|
+
block = stream.read(CHUNK_BYTES)
|
|
133
|
+
_require(time.monotonic() < deadline, "model_staging_timeout")
|
|
134
|
+
_require(type(block) is bytes and len(block) <= CHUNK_BYTES, "invalid_model_stream")
|
|
135
|
+
if not block:
|
|
136
|
+
break
|
|
137
|
+
observed += len(block)
|
|
138
|
+
_require(observed <= receipt.size_bytes, "model_stream_size_mismatch")
|
|
139
|
+
digest.update(block); target.write(block)
|
|
140
|
+
target.flush(); os.fsync(target.fileno())
|
|
141
|
+
_require(observed == receipt.size_bytes, "model_stream_size_mismatch")
|
|
142
|
+
_require("sha256:" + digest.hexdigest() == receipt.digest, "model_stream_digest_mismatch")
|
|
143
|
+
# The directory is fresh and receipt names come from the fixed local
|
|
144
|
+
# inventory, so a previous file cannot be silently replaced.
|
|
145
|
+
_require(not destination.exists(), "model_staging_conflict")
|
|
146
|
+
temporary.rename(destination)
|
|
147
|
+
except HostedError:
|
|
148
|
+
raise
|
|
149
|
+
except Exception:
|
|
150
|
+
raise HostedError("model_store_unavailable", "registered model object retrieval failed") from None
|
|
151
|
+
finally:
|
|
152
|
+
if temporary.is_file():
|
|
153
|
+
_require(temporary.resolve().is_relative_to(destination.parent.resolve()), "invalid_model_staging_path")
|
|
154
|
+
temporary.unlink()
|
|
155
|
+
|
|
156
|
+
def stage(self, output_dir):
|
|
157
|
+
"""Create a fresh validated package; return only after all files pass.
|
|
158
|
+
|
|
159
|
+
On failure the unadmitted fresh directory can retain successfully staged
|
|
160
|
+
objects; no success receipt is returned. Read-only mode is a file guard,
|
|
161
|
+
not a substitute for a read-only mount and an unprivileged sandbox UID.
|
|
162
|
+
"""
|
|
163
|
+
output = Path(output_dir).absolute()
|
|
164
|
+
_require(not output.exists(), "model_staging_exists")
|
|
165
|
+
output.mkdir(parents=True, mode=0o700, exist_ok=False)
|
|
166
|
+
root = models._root(output)
|
|
167
|
+
deadline = time.monotonic() + STAGING_TIMEOUT_SECONDS
|
|
168
|
+
for relative, receipt in self.registration.files:
|
|
169
|
+
destination = root.joinpath(*relative.split("/"))
|
|
170
|
+
destination.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
171
|
+
_require(destination.parent.resolve().is_relative_to(root), "invalid_model_staging_path")
|
|
172
|
+
self._stream(receipt, destination, deadline)
|
|
173
|
+
verified = models.load_model_artifact(root, expected_manifest_sha256=self.registration.manifest_digest,
|
|
174
|
+
registered_asset_locators=models.REGISTERED_MODEL_LOCATORS)
|
|
175
|
+
for path in root.rglob("*"):
|
|
176
|
+
if path.is_file():
|
|
177
|
+
path.chmod(0o444)
|
|
178
|
+
for path in sorted((path for path in root.rglob("*") if path.is_dir()), key=lambda path: len(path.parts), reverse=True):
|
|
179
|
+
path.chmod(0o555)
|
|
180
|
+
root.chmod(0o555)
|
|
181
|
+
return verified
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Additive model-session provenance; metadata never proves actual weight use."""
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from ..contracts import canonical_json,ContractError
|
|
7
|
+
from .contracts import HostedError
|
|
8
|
+
from .model_session import SESSION_POLICY,TIMING_POLICY
|
|
9
|
+
|
|
10
|
+
SCHEMA="sphynx.hosted.model-session.reproducibility.v1"
|
|
11
|
+
PREPARED_SCHEMA="sphynx.hosted.prepared-submission.v3"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require(ok):
|
|
15
|
+
if not ok: raise HostedError("model_history_identity_mismatch","model session history differs from the operator-pinned closure")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def same_json(a,b):
|
|
19
|
+
try:return canonical_json(a)==canonical_json(b)
|
|
20
|
+
except (ValueError,TypeError,RecursionError):return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_model_history_identity(manifest,runtime):
|
|
24
|
+
from .model_admission import validate_model_runtime
|
|
25
|
+
_require(type(manifest) is dict and manifest.get("schema_version")==PREPARED_SCHEMA)
|
|
26
|
+
admission=validate_model_runtime(manifest.get("model_runtime"))
|
|
27
|
+
_require(type(runtime) is dict and same_json(runtime.get("resource_policy"),SESSION_POLICY)
|
|
28
|
+
and same_json(runtime.get("timing_policy"),TIMING_POLICY)
|
|
29
|
+
and runtime.get("image_digest")==admission["image_digest"]
|
|
30
|
+
and runtime.get("model_manifest_digest")==admission["manifest_digest"]
|
|
31
|
+
and type(runtime.get("runsc_sha256")) is str and re.fullmatch("[0-9a-f]{64}",runtime["runsc_sha256"]))
|
|
32
|
+
config=manifest.get("candidate_configuration")
|
|
33
|
+
_require(type(config) is dict and config.get("model")=={"kind":"registered","ref":admission["model_ref"]}
|
|
34
|
+
and config.get("runtime")=={"profile":"python-minilm-cpu.v1","python":"3.12","transport":"jsonl-session.v1"})
|
|
35
|
+
return admission
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def model_reproduction_record(manifest,runtime,*,helper_digest,baseline_digest,requests,platform=None):
|
|
39
|
+
admission=validate_model_history_identity(manifest,runtime)
|
|
40
|
+
_require(type(requests) is list and all(type(v) is dict for v in requests)
|
|
41
|
+
and all(type(v) is str and re.fullmatch("sha256:[0-9a-f]{64}",v) for v in (helper_digest,baseline_digest)))
|
|
42
|
+
config=manifest["candidate_configuration"]
|
|
43
|
+
method=config.get("method")
|
|
44
|
+
record={"schema_version":SCHEMA,
|
|
45
|
+
"candidate":{"pr_url":manifest["candidate_pr"],"repository":deepcopy(manifest["head_repository"]),
|
|
46
|
+
"commit_sha":manifest["head_sha"],"tree_sha":manifest["tree_sha"],
|
|
47
|
+
"configuration_digest":manifest["configuration_digest"],"source_bundle_digest":manifest["source_bundle_digest"],
|
|
48
|
+
**deepcopy(config["candidate"]),"files":deepcopy(manifest["files"])},
|
|
49
|
+
"method":{"declaration_status":"declared" if method else "not_declared","declaration":deepcopy(method),"verified_against_algorithm":False},
|
|
50
|
+
"model":{"kind":"registered","identity":admission["model_ref"],"declaration":deepcopy(config["model"]),
|
|
51
|
+
"manifest_digest":admission["manifest_digest"],"artifacts":deepcopy(admission["model_assets"]),
|
|
52
|
+
"operator_admission":deepcopy(admission),"uses_learned_weights_declared":True,
|
|
53
|
+
"actual_weight_use_verified":False,"session_ready_proves_model_use":False},
|
|
54
|
+
"build":deepcopy(config["build"]),"runtime":deepcopy(runtime),"platform":deepcopy(platform),
|
|
55
|
+
"evaluation":{**deepcopy(manifest["suite"]),"input_order_digest":"sha256:"+hashlib.sha256(canonical_json(requests).encode()).hexdigest(),
|
|
56
|
+
"input_contract_digest":helper_digest,"reference_labels_visible_to_candidate":False},
|
|
57
|
+
"baseline":{**deepcopy(manifest["baseline"]),"source_digest":baseline_digest,"model_use_claimed":False},
|
|
58
|
+
"reproduction":{"level":"operator_artifact_closure_and_runtime_identities_recorded","bitwise_repeatability_verified":False,
|
|
59
|
+
"model_weights_embedded_in_report":False,"limitations":[
|
|
60
|
+
"Readiness and artifact availability do not prove submitted code used those weights.",
|
|
61
|
+
"The trusted probe verifies its reviewed probe program; it is not a proof about every submitted candidate.",
|
|
62
|
+
"Model files, wheel closure, image and probe receipts require independent retention and verified recovery.",
|
|
63
|
+
"Host request wall includes tokenization, inference, serialization and IPC; pure tensor inference is not isolated."]}}
|
|
64
|
+
if "candidate_family" in manifest:
|
|
65
|
+
record.update(candidate_family=deepcopy(manifest["candidate_family"]),candidate_method=manifest["candidate_method"])
|
|
66
|
+
return record
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_model_report_metadata(report):
|
|
70
|
+
"""Check recorded closure bindings; report/capture bytes need trusted storage verification."""
|
|
71
|
+
admission=validate_model_history_identity(report.get("manifest"),report.get("runtime_identity"))
|
|
72
|
+
recorded=report.get("reproducibility")
|
|
73
|
+
_require(type(recorded) is dict and recorded.get("schema_version")==SCHEMA
|
|
74
|
+
and type(recorded.get("evaluation")) is dict)
|
|
75
|
+
input_digest=recorded["evaluation"].get("input_order_digest")
|
|
76
|
+
_require(type(input_digest) is str and re.fullmatch("sha256:[0-9a-f]{64}",input_digest))
|
|
77
|
+
# The actual input byte/digest check occurs during grading. At comparison,
|
|
78
|
+
# this verifies every other provenance field without inventing input bytes.
|
|
79
|
+
expected=model_reproduction_record(report["manifest"],report["runtime_identity"],
|
|
80
|
+
helper_digest=report.get("helper_digest"),baseline_digest=report.get("baseline_source_digest"),
|
|
81
|
+
requests=[],platform=recorded.get("platform"))
|
|
82
|
+
expected["evaluation"]["input_order_digest"]=input_digest
|
|
83
|
+
_require(recorded==expected)
|
|
84
|
+
return admission
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Bounded persistent guest transport for a separately admitted CPU model.
|
|
2
|
+
|
|
3
|
+
This module does not admit an image or model. A trusted operator must verify the
|
|
4
|
+
runtime and artifact closure before constructing it. Readiness is a submitted
|
|
5
|
+
program assertion; recorded model availability is not proof of algorithm use.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import queue
|
|
13
|
+
import subprocess
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from ..contracts import canonical_json, validate_candidate_request, validate_candidate_response
|
|
19
|
+
from .contracts import HostedError
|
|
20
|
+
from .sandbox import GVisorSandbox, MAX_STREAM
|
|
21
|
+
|
|
22
|
+
SESSION_POLICY = {"id": "sphynx.gvisor.cpu.model-session.v1", "cpus": 1,
|
|
23
|
+
"memory_bytes": 2_147_483_648, "pids": 64, "tmpfs_bytes": 67_108_864,
|
|
24
|
+
"case_timeout_seconds": 30, "build_step_timeout_seconds": 30,
|
|
25
|
+
"initialization_timeout_seconds": 120, "maximum_requests": 30,
|
|
26
|
+
"network": "none", "user": "65532:65532", "root_filesystem": "read_only",
|
|
27
|
+
"fresh_process_per_case": False, "threads": 1}
|
|
28
|
+
TIMING_POLICY = {"id": "trusted-model-session-wall.v1", "unit": "ms",
|
|
29
|
+
"initialization": "container_start_to_declared_ready_including_imports_and_model_loading",
|
|
30
|
+
"request": "host_write_to_complete_response_including_tokenization_inference_serialization_and_ipc",
|
|
31
|
+
"pure_tensor_inference_measured": False, "ready_proves_model_use": False}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ModelSessionSandbox(GVisorSandbox):
|
|
35
|
+
def __init__(self, image, *, expected_runsc_sha256, model_path, model_manifest_digest, runsc_path="/usr/local/bin/runsc"):
|
|
36
|
+
super().__init__(image, expected_runsc_sha256=expected_runsc_sha256, runsc_path=runsc_path)
|
|
37
|
+
from re import fullmatch
|
|
38
|
+
if not fullmatch("sha256:[0-9a-f]{64}", model_manifest_digest):
|
|
39
|
+
raise HostedError("invalid_model_identity", "an independently admitted model manifest is required")
|
|
40
|
+
model_path = Path(model_path)
|
|
41
|
+
if not model_path.is_dir() or model_path.is_symlink():
|
|
42
|
+
raise HostedError("invalid_model_identity", "verified read-only model files are required")
|
|
43
|
+
self.model_path, self.model_manifest_digest = model_path, model_manifest_digest
|
|
44
|
+
|
|
45
|
+
def identity(self):
|
|
46
|
+
return {"image_digest": self.image, "runsc_sha256": self.runsc_hash,
|
|
47
|
+
"resource_policy": dict(SESSION_POLICY), "timing_policy": dict(TIMING_POLICY),
|
|
48
|
+
"model_manifest_digest": self.model_manifest_digest}
|
|
49
|
+
|
|
50
|
+
def command(self, argv, source, helper, name):
|
|
51
|
+
command = super().command(argv, source, helper, name)
|
|
52
|
+
command[command.index("--memory=1g")] = "--memory=2g"
|
|
53
|
+
command[command.index("--memory-swap=1g")] = "--memory-swap=2g"
|
|
54
|
+
index = command.index("-i")
|
|
55
|
+
additions = ["--mount", f"type=bind,source={self.model_path.resolve()},target=/model,readonly"]
|
|
56
|
+
for value in ("HF_HUB_OFFLINE=1", "TRANSFORMERS_OFFLINE=1", "HF_HUB_DISABLE_TELEMETRY=1", "TOKENIZERS_PARALLELISM=false",
|
|
57
|
+
"OMP_NUM_THREADS=1", "MKL_NUM_THREADS=1", "OPENBLAS_NUM_THREADS=1",
|
|
58
|
+
"SPHYNX_MODEL_MANIFEST_DIGEST="+self.model_manifest_digest):
|
|
59
|
+
additions.append("--env="+value)
|
|
60
|
+
return command[:index]+additions+command[index:]
|
|
61
|
+
|
|
62
|
+
def open_session(self, argv, source, helper, *, initialization_timeout=120):
|
|
63
|
+
self.verify()
|
|
64
|
+
name = "sphynx-model-"+uuid.uuid4().hex
|
|
65
|
+
return GuestSession(self.command(argv, source, helper, name), name,
|
|
66
|
+
self.model_manifest_digest, initialization_timeout=initialization_timeout)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GuestSession:
|
|
70
|
+
"""One bounded JSON line at a time; timeout kills the guest, not just Docker."""
|
|
71
|
+
def __init__(self, command, name, manifest_digest, *, launch=subprocess.Popen, remove=None, clock=time.monotonic,
|
|
72
|
+
initialization_timeout=120):
|
|
73
|
+
if type(initialization_timeout) not in (int, float) or not 0 < initialization_timeout <= SESSION_POLICY["initialization_timeout_seconds"]:
|
|
74
|
+
raise HostedError("invalid_session_timeout", "initialization timeout exceeds the runtime policy")
|
|
75
|
+
self.command, self.name, self.manifest_digest = command, name, manifest_digest
|
|
76
|
+
self.launch, self.clock = launch, clock
|
|
77
|
+
self.remove = remove or (lambda: subprocess.run(["docker", "rm", "-f", name],
|
|
78
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20, check=False))
|
|
79
|
+
self.process = None
|
|
80
|
+
self.frames = queue.Queue(maxsize=2)
|
|
81
|
+
self.stderr = bytearray()
|
|
82
|
+
self.failed = threading.Event()
|
|
83
|
+
self.stream_ended = threading.Event()
|
|
84
|
+
self.closed = False
|
|
85
|
+
self.requests = 0
|
|
86
|
+
self.initialization_wall_ms = None
|
|
87
|
+
self.threads = []
|
|
88
|
+
self.initialization_timeout = initialization_timeout
|
|
89
|
+
self._close_lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
def _stdout(self):
|
|
92
|
+
pending = bytearray()
|
|
93
|
+
try:
|
|
94
|
+
read = getattr(self.process.stdout, "read1", self.process.stdout.read)
|
|
95
|
+
while block := read(8192):
|
|
96
|
+
pending.extend(block)
|
|
97
|
+
while b"\n" in pending:
|
|
98
|
+
line, _, rest = pending.partition(b"\n")
|
|
99
|
+
pending = bytearray(rest)
|
|
100
|
+
if len(line) > MAX_STREAM:
|
|
101
|
+
raise ValueError("frame limit")
|
|
102
|
+
self.frames.put_nowait(bytes(line))
|
|
103
|
+
if len(pending) > MAX_STREAM:
|
|
104
|
+
raise ValueError("frame limit")
|
|
105
|
+
if pending:
|
|
106
|
+
raise ValueError("unterminated frame")
|
|
107
|
+
except (ValueError, queue.Full, OSError):
|
|
108
|
+
self.failed.set()
|
|
109
|
+
self.process.kill()
|
|
110
|
+
finally:
|
|
111
|
+
self.stream_ended.set()
|
|
112
|
+
try: self.frames.put_nowait(None)
|
|
113
|
+
except queue.Full: pass
|
|
114
|
+
|
|
115
|
+
def _stderr(self):
|
|
116
|
+
try:
|
|
117
|
+
read = getattr(self.process.stderr, "read1", self.process.stderr.read)
|
|
118
|
+
while block := read(8192):
|
|
119
|
+
room = MAX_STREAM-len(self.stderr)
|
|
120
|
+
self.stderr.extend(block[:room])
|
|
121
|
+
if len(block)>room:
|
|
122
|
+
self.failed.set(); self.process.kill(); return
|
|
123
|
+
except OSError:
|
|
124
|
+
self.failed.set()
|
|
125
|
+
|
|
126
|
+
def _frame(self, timeout):
|
|
127
|
+
try:
|
|
128
|
+
frame = self.frames.get(timeout=timeout)
|
|
129
|
+
except queue.Empty:
|
|
130
|
+
raise HostedError("candidate_session_timeout", "guest did not produce a bounded frame in time") from None
|
|
131
|
+
if frame is None or self.failed.is_set():
|
|
132
|
+
raise HostedError("candidate_session_failed", "guest stream ended or exceeded its bounds")
|
|
133
|
+
return frame
|
|
134
|
+
|
|
135
|
+
def __enter__(self):
|
|
136
|
+
started = self.clock()
|
|
137
|
+
with self._close_lock:
|
|
138
|
+
if self.closed:
|
|
139
|
+
raise HostedError("candidate_session_closed", "session was cancelled before launch")
|
|
140
|
+
try:
|
|
141
|
+
self.process = self.launch(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
142
|
+
stderr=subprocess.PIPE, env={key:value for key,value in os.environ.items() if key in ("PATH", "LANG")})
|
|
143
|
+
except OSError:
|
|
144
|
+
raise HostedError("sandbox_launch_failed", "trusted session could not launch") from None
|
|
145
|
+
self.threads = [threading.Thread(target=self._stdout, daemon=True), threading.Thread(target=self._stderr, daemon=True)]
|
|
146
|
+
for thread in self.threads: thread.start()
|
|
147
|
+
try:
|
|
148
|
+
try:
|
|
149
|
+
ready = _session_json(self._frame(self.initialization_timeout))
|
|
150
|
+
except (ValueError, TypeError, UnicodeError, RecursionError):
|
|
151
|
+
raise HostedError("invalid_session_ready", "guest readiness is not strict bounded JSON") from None
|
|
152
|
+
if ready != {"schema_version":"sphynx.rag.rerank.session.ready.v1", "model_manifest_digest":self.manifest_digest}:
|
|
153
|
+
raise HostedError("invalid_session_ready", "guest readiness differs from the pinned model identity")
|
|
154
|
+
self.initialization_wall_ms = (self.clock()-started)*1000
|
|
155
|
+
except BaseException:
|
|
156
|
+
self.close()
|
|
157
|
+
raise
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def request(self, value, *, timeout=30):
|
|
161
|
+
if self.closed or self.requests >= SESSION_POLICY["maximum_requests"]:
|
|
162
|
+
raise HostedError("candidate_session_closed", "session is closed or has reached its request limit")
|
|
163
|
+
if self.process is None or self.stream_ended.is_set() or self.failed.is_set() or self.process.poll() is not None:
|
|
164
|
+
self.close()
|
|
165
|
+
raise HostedError("candidate_session_closed", "guest has ended before another request could be attempted")
|
|
166
|
+
if type(timeout) not in (int, float) or not 0 < timeout <= SESSION_POLICY["case_timeout_seconds"]:
|
|
167
|
+
raise HostedError("invalid_session_timeout", "request timeout exceeds the runtime policy")
|
|
168
|
+
request = validate_candidate_request(value)
|
|
169
|
+
raw = (request.to_json()+"\n").encode()
|
|
170
|
+
if len(raw)>MAX_STREAM:
|
|
171
|
+
raise HostedError("candidate_input_limit", "session input exceeds its bound")
|
|
172
|
+
self.requests += 1
|
|
173
|
+
started = self.clock()
|
|
174
|
+
write_failed = threading.Event()
|
|
175
|
+
def write():
|
|
176
|
+
try:
|
|
177
|
+
self.process.stdin.write(raw); self.process.stdin.flush()
|
|
178
|
+
except (BrokenPipeError, OSError, ValueError): write_failed.set()
|
|
179
|
+
writer = threading.Thread(target=write, daemon=True); writer.start()
|
|
180
|
+
status, stdout = "ok", b""
|
|
181
|
+
try:
|
|
182
|
+
stdout = self._frame(timeout)
|
|
183
|
+
response = _session_json(stdout)
|
|
184
|
+
validate_candidate_response(response, request)
|
|
185
|
+
if write_failed.is_set():
|
|
186
|
+
raise HostedError("candidate_session_failed", "guest could not receive the request")
|
|
187
|
+
except HostedError as exc:
|
|
188
|
+
status = "timeout" if exc.code == "candidate_session_timeout" else "process_failed"
|
|
189
|
+
self.close()
|
|
190
|
+
except (ValueError, TypeError, KeyError, UnicodeError, RecursionError):
|
|
191
|
+
# Keep the raw invalid response for separate grading.
|
|
192
|
+
self.close()
|
|
193
|
+
ended = self.clock()
|
|
194
|
+
writer.join(1)
|
|
195
|
+
return {"status":status, "stdout_base64":base64.b64encode(stdout).decode(),
|
|
196
|
+
"stderr_base64":base64.b64encode(bytes(self.stderr)).decode(), "exit_code":self.process.poll(),
|
|
197
|
+
"runner_latency_ms":(ended-started)*1000, "timing_policy":dict(TIMING_POLICY)}
|
|
198
|
+
|
|
199
|
+
def stderr_snapshot(self):
|
|
200
|
+
"""Bounded session stderr; the collector archives it once after close."""
|
|
201
|
+
return bytes(self.stderr)
|
|
202
|
+
|
|
203
|
+
def close(self):
|
|
204
|
+
with self._close_lock:
|
|
205
|
+
if self.closed: return
|
|
206
|
+
self.closed = True
|
|
207
|
+
if self.process is not None:
|
|
208
|
+
try: self.process.kill()
|
|
209
|
+
except OSError: pass
|
|
210
|
+
try: self.process.wait(timeout=5)
|
|
211
|
+
except (OSError, subprocess.TimeoutExpired): pass
|
|
212
|
+
try: self.remove()
|
|
213
|
+
finally:
|
|
214
|
+
for thread in self.threads: thread.join(1)
|
|
215
|
+
if self.process is not None:
|
|
216
|
+
for stream in (self.process.stdin,self.process.stdout,self.process.stderr):
|
|
217
|
+
try: stream.close()
|
|
218
|
+
except OSError: pass
|
|
219
|
+
|
|
220
|
+
def __exit__(self, *_):
|
|
221
|
+
self.close()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _session_json(raw):
|
|
225
|
+
"""Reject duplicate keys and non-finite numbers in ready and reply frames."""
|
|
226
|
+
def unique(pairs):
|
|
227
|
+
value = {}
|
|
228
|
+
for key, item in pairs:
|
|
229
|
+
if key in value: raise ValueError("duplicate frame field")
|
|
230
|
+
value[key] = item
|
|
231
|
+
return value
|
|
232
|
+
return json.loads(raw, object_pairs_hook=unique,
|
|
233
|
+
parse_constant=lambda _: (_ for _ in ()).throw(ValueError("non-finite frame value")))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Installed public reranking contract; admission metadata is not a dataset."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..benchmarks import BenchmarkRegistry, BenchmarkRelease, LabelSetRelease
|
|
9
|
+
from ..evals import (ArtifactBinding, ArtifactReference, EvaluationContractError,
|
|
10
|
+
EvaluationDefinition, require_legacy_rerank, resolve_eval)
|
|
11
|
+
from .contracts import HostedError
|
|
12
|
+
from ..public_eval import (
|
|
13
|
+
EVALUATION_ID, BENCHMARK_REF, LABEL_REF, MANIFEST_DIGEST, BENCHMARK_DIGEST,
|
|
14
|
+
LABEL_DIGEST, GRADER_RELEASE, METRIC_RELEASE, INPUT_DIGEST,
|
|
15
|
+
public_definition, public_artifacts,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def public_release_metadata():
|
|
21
|
+
"""Read a reviewed content-free catalog, never corpus or reference bytes."""
|
|
22
|
+
path = Path(__file__).resolve().parents[1] / "data/public-benchmark-release.v1.json"
|
|
23
|
+
value = json.loads(path.read_bytes())
|
|
24
|
+
if (value.get("schema_version") != "sphynx.hosted.public-admission.v1"
|
|
25
|
+
or value.get("manifest_digest") != MANIFEST_DIGEST):
|
|
26
|
+
raise HostedError("public_admission_identity_mismatch", "installed benchmark metadata differs")
|
|
27
|
+
benchmark = BenchmarkRelease.from_dict(value["benchmark_release"])
|
|
28
|
+
labels = LabelSetRelease.from_dict(value["label_release"])
|
|
29
|
+
registry = BenchmarkRegistry((labels,), (benchmark,))
|
|
30
|
+
if (benchmark.ref != BENCHMARK_REF or benchmark.digest != BENCHMARK_DIGEST
|
|
31
|
+
or labels.ref != LABEL_REF or labels.digest != LABEL_DIGEST
|
|
32
|
+
or benchmark.evaluation_id != EVALUATION_ID or benchmark.input.digest != INPUT_DIGEST
|
|
33
|
+
or not registry.resolve(BENCHMARK_REF, {"split": "development"}).eligible):
|
|
34
|
+
raise HostedError("public_admission_identity_mismatch", "installed release is not eligible")
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def require_hosted_rerank(evaluation_id, parameters=None):
|
|
45
|
+
if evaluation_id == "rag.rerank.v1":
|
|
46
|
+
return require_legacy_rerank(evaluation_id, parameters)
|
|
47
|
+
if evaluation_id != EVALUATION_ID:
|
|
48
|
+
raise EvaluationContractError("evaluation does not have an admitted hosted adapter")
|
|
49
|
+
public_release_metadata()
|
|
50
|
+
return resolve_eval(evaluation_id, parameters)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_public_inputs(raw, manifest):
|
|
54
|
+
"""Worker verifies label-free inputs without fetching reference assets."""
|
|
55
|
+
from ..contracts import validate_candidate_request
|
|
56
|
+
from .benchmark_intake import read_json
|
|
57
|
+
if (type(raw) is not bytes or "sha256:" + hashlib.sha256(raw).hexdigest() != INPUT_DIGEST
|
|
58
|
+
or manifest.get("benchmark_manifest_digest") != MANIFEST_DIGEST
|
|
59
|
+
or manifest.get("benchmark_release", {}).get("digest") != BENCHMARK_DIGEST
|
|
60
|
+
or manifest.get("label_set_release", {}).get("digest") != LABEL_DIGEST):
|
|
61
|
+
raise HostedError("public_input_identity_mismatch", "public inputs differ from the admitted release")
|
|
62
|
+
inputs = read_json(raw)
|
|
63
|
+
if (inputs.get("schema_version") != "sphynx.hosted.public-rerank.inputs.v1"
|
|
64
|
+
or inputs.get("benchmark_ref") != BENCHMARK_REF or len(inputs.get("requests", [])) != 15):
|
|
65
|
+
raise HostedError("public_input_identity_mismatch", "public input membership differs")
|
|
66
|
+
metadata = public_release_metadata()
|
|
67
|
+
expected = metadata["benchmark_release"]["cases"]
|
|
68
|
+
requests = inputs["requests"]
|
|
69
|
+
if [r.get("case_id") for r in requests] != [case["case_id"] for case in expected]:
|
|
70
|
+
raise HostedError("public_input_identity_mismatch", "public case order differs")
|
|
71
|
+
for request in requests:
|
|
72
|
+
if request != validate_candidate_request(request).to_dict():
|
|
73
|
+
raise HostedError("public_input_identity_mismatch", "candidate input contract differs")
|
|
74
|
+
return requests
|