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.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. 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