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,97 @@
1
+ """Operator-owned repository authorization; never supplied by a candidate PR."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from .contracts import HostedError, fields, stable_id
10
+
11
+ REGISTRY_SCHEMA = "sphynx.hosted.registry.v1"
12
+ SUITE_ID = "rag.rerank.v1"
13
+ BASELINE_ID = "supplied-score:v1"
14
+ PUBLIC_SUITE_ID = "rag.rerank.public.v1"
15
+ REGISTERED_MAIN_BASELINE_ID = "registered-main-reflex-lexical:v1"
16
+ _REPOSITORY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$")
17
+
18
+
19
+ def repository_name(value: object) -> str:
20
+ if not isinstance(value, str) or not _REPOSITORY.fullmatch(value):
21
+ raise HostedError("invalid_repository", "repository must be an owner/name pair")
22
+ return value
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class RepositoryRegistration:
27
+ repository_id: int
28
+ full_name: str
29
+ team_id: str
30
+ allowed_suites: tuple[str, ...]
31
+ allowed_baselines: tuple[str, ...]
32
+
33
+ def __post_init__(self):
34
+ if type(self.repository_id) is not int or self.repository_id <= 0:
35
+ raise HostedError("invalid_registry", "repository_id must be a positive GitHub repository ID")
36
+ repository_name(self.full_name)
37
+ stable_id(self.team_id, "team_id")
38
+ for values, supported in ((self.allowed_suites, {SUITE_ID, PUBLIC_SUITE_ID}),
39
+ (self.allowed_baselines, {BASELINE_ID, REGISTERED_MAIN_BASELINE_ID})):
40
+ if (not isinstance(values, tuple) or not values or
41
+ any(type(value) is not str or value not in supported for value in values)
42
+ or len(set(values)) != len(values)):
43
+ raise HostedError("invalid_registry", "suite and baseline grants must name explicit admitted IDs without duplicates")
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class HostedRegistry:
48
+ repositories: tuple[RepositoryRegistration, ...]
49
+
50
+ def __post_init__(self):
51
+ if not isinstance(self.repositories, tuple) or not self.repositories:
52
+ raise HostedError("invalid_registry", "at least one explicit repository registration is required")
53
+ if any(not isinstance(repo, RepositoryRegistration) for repo in self.repositories):
54
+ raise HostedError("invalid_registry", "invalid repository registration")
55
+ ids = [repo.repository_id for repo in self.repositories]
56
+ names = [repo.full_name.casefold() for repo in self.repositories]
57
+ if len(set(ids)) != len(ids) or len(set(names)) != len(names):
58
+ raise HostedError("invalid_registry", "repository IDs and names must be unique")
59
+
60
+ def authorize(self, team_id: str, full_name: str, suite: str, baseline: str,
61
+ *, repository_id: int | None = None) -> RepositoryRegistration:
62
+ # Give one generic denial for unauthorized teams, repositories, and data.
63
+ match = next((repo for repo in self.repositories
64
+ if repo.full_name.casefold() == full_name.casefold() and repo.team_id == team_id), None)
65
+ if match is None or suite not in match.allowed_suites or baseline not in match.allowed_baselines:
66
+ raise HostedError("submission_not_authorized", "team, repository, suite, or baseline is not registered")
67
+ if repository_id is not None and match.repository_id != repository_id:
68
+ raise HostedError("repository_identity_mismatch", "GitHub repository ID differs from its registration")
69
+ return match
70
+
71
+
72
+ def load_registry(path: str | Path) -> HostedRegistry:
73
+ try:
74
+ payload = Path(path).read_bytes()
75
+ if len(payload) > 262_144:
76
+ raise ValueError("size")
77
+ def unique(pairs):
78
+ result = {}
79
+ for key, value in pairs:
80
+ if key in result:
81
+ raise ValueError("duplicate")
82
+ result[key] = value
83
+ return result
84
+ raw = json.loads(payload, object_pairs_hook=unique)
85
+ except (OSError, ValueError, RecursionError):
86
+ raise HostedError("invalid_registry", "registry must be bounded JSON without duplicate fields") from None
87
+ fields(raw, {"schema_version", "repositories"}, "registry")
88
+ if raw["schema_version"] != REGISTRY_SCHEMA or not isinstance(raw["repositories"], list):
89
+ raise HostedError("invalid_registry", "unsupported registry schema")
90
+ registrations = []
91
+ for item in raw["repositories"]:
92
+ item = fields(item, {"repository_id", "full_name", "team_id", "allowed_suites", "allowed_baselines"}, "repository")
93
+ if not isinstance(item["allowed_suites"], list) or not isinstance(item["allowed_baselines"], list):
94
+ raise HostedError("invalid_registry", "suite and baseline permissions must be arrays")
95
+ registrations.append(RepositoryRegistration(item["repository_id"], item["full_name"], item["team_id"],
96
+ tuple(item["allowed_suites"]), tuple(item["allowed_baselines"])))
97
+ return HostedRegistry(tuple(registrations))
@@ -0,0 +1,126 @@
1
+ """Immutable source revision checks shared by PR and registered-main resolution.
2
+
3
+ Resolving a baseline source is deliberately separate from admitting a runnable
4
+ baseline. It does not fetch models, archive source, register an execution adapter,
5
+ or enable main-branch model execution in the current synthetic pilot.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timezone
12
+ from typing import Any, Mapping, Protocol
13
+
14
+ from .contracts import HostedError
15
+ from .registry import RepositoryRegistration
16
+
17
+ _COMMIT_SHA = re.compile(r"[0-9a-fA-F]{40}", re.ASCII)
18
+
19
+
20
+ def require_commit_sha(value: object, *, field: str = "commit_sha") -> str:
21
+ """Accept a complete Git SHA-1 object identity; never accept a moving ref."""
22
+ if not isinstance(value, str) or _COMMIT_SHA.fullmatch(value) is None:
23
+ raise HostedError("invalid_commit_sha", f"{field} must be a complete 40-character hexadecimal commit SHA")
24
+ return value.lower()
25
+
26
+
27
+ def enforce_expected_sha(actual_sha: object, expected_sha: object | None, *,
28
+ field: str = "expected_head_sha",
29
+ mismatch_code: str = "expected_head_mismatch") -> str:
30
+ """Actual identity is mandatory; a caller's matching constraint is optional."""
31
+ actual = require_commit_sha(actual_sha, field="resolved_commit_sha")
32
+ if expected_sha is not None and require_commit_sha(expected_sha, field=field) != actual:
33
+ raise HostedError(mismatch_code,
34
+ f"resolved revision differs from {field}; confirm the intended revision and submit a new experiment")
35
+ return actual
36
+
37
+
38
+ class RegisteredMainReader(Protocol):
39
+ def verify_repository_scope(self, full_name: str) -> Mapping[str, Any]: ...
40
+ def get_json(self, path: str) -> Mapping[str, Any]: ...
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ResolvedMainRevision:
45
+ """A source-only record; a future baseline manifest must also bind artifacts."""
46
+ repository: str
47
+ repository_id: int
48
+ team_id: str
49
+ head_sha: str
50
+ tree_sha: str
51
+ resolved_at_utc: str
52
+ expected_sha: str | None = None
53
+
54
+ def __post_init__(self):
55
+ if type(self.repository_id) is not int or self.repository_id <= 0:
56
+ raise HostedError("invalid_github_identity", "resolved repository ID is invalid")
57
+ # Use the same checks for restored records as for new resolutions.
58
+ from .registry import repository_name
59
+ repository_name(self.repository)
60
+ if not isinstance(self.team_id, str) or not self.team_id:
61
+ raise HostedError("invalid_recorded_head", "resolved team identity is missing")
62
+ object.__setattr__(self, "head_sha", enforce_expected_sha(
63
+ self.head_sha, self.expected_sha, field="baseline_expected_sha",
64
+ mismatch_code="expected_baseline_mismatch"))
65
+ object.__setattr__(self, "tree_sha", require_commit_sha(self.tree_sha, field="tree_sha"))
66
+ if self.expected_sha is not None:
67
+ object.__setattr__(self, "expected_sha", require_commit_sha(self.expected_sha, field="baseline_expected_sha"))
68
+ try:
69
+ stamp = datetime.fromisoformat(self.resolved_at_utc.replace("Z", "+00:00"))
70
+ if stamp.utcoffset() is None:
71
+ raise ValueError("timezone")
72
+ except (AttributeError, TypeError, ValueError):
73
+ raise HostedError("invalid_recorded_head", "resolved timestamp must include a timezone") from None
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ return {"schema_version": "sphynx.registered-main-revision.v1",
77
+ "repository": self.repository, "repository_id": self.repository_id,
78
+ "team_id": self.team_id, "ref": "refs/heads/main",
79
+ "head_sha": self.head_sha, "tree_sha": self.tree_sha,
80
+ "resolved_at_utc": self.resolved_at_utc, "expected_sha": self.expected_sha,
81
+ "scope": "source_revision_only", "execution_enabled": False}
82
+
83
+ @classmethod
84
+ def from_dict(cls, value: Mapping[str, Any]) -> "ResolvedMainRevision":
85
+ fields = {"schema_version", "repository", "repository_id", "team_id", "ref",
86
+ "head_sha", "tree_sha", "resolved_at_utc", "expected_sha", "scope", "execution_enabled"}
87
+ if (not isinstance(value, Mapping) or set(value) != fields or
88
+ value["schema_version"] != "sphynx.registered-main-revision.v1" or
89
+ value["ref"] != "refs/heads/main" or value["scope"] != "source_revision_only" or
90
+ value["execution_enabled"] is not False):
91
+ raise HostedError("invalid_recorded_head", "registered main revision has an unsupported shape")
92
+ return cls(**{key: value[key] for key in ("repository", "repository_id", "team_id", "head_sha",
93
+ "tree_sha", "resolved_at_utc", "expected_sha")})
94
+
95
+
96
+ def resolve_registered_main(reader: RegisteredMainReader, registration: RepositoryRegistration, *,
97
+ team_id: str, expected_sha: object | None = None) -> ResolvedMainRevision:
98
+ """Resolve an operator-registered repository's main exactly once.
99
+
100
+ The caller must persist the returned record before archive/build/queue work.
101
+ Retries reuse that record instead of resolving main again. Baseline execution
102
+ additionally needs its own authorized definition, immutable model package,
103
+ and supported adapter; repository source permission alone grants none of them.
104
+ """
105
+ if not isinstance(registration, RepositoryRegistration) or registration.team_id != team_id:
106
+ raise HostedError("submission_not_authorized", "repository is not registered for this team")
107
+ expected = None if expected_sha is None else require_commit_sha(expected_sha, field="baseline_expected_sha")
108
+ scope = reader.verify_repository_scope(registration.full_name)
109
+ if (not isinstance(scope, Mapping) or type(scope.get("repository_id")) is not int or
110
+ scope["repository_id"] != registration.repository_id or
111
+ not isinstance(scope.get("full_name"), str) or
112
+ scope["full_name"].casefold() != registration.full_name.casefold() or
113
+ scope.get("scoped_repository_count") != 1):
114
+ raise HostedError("repository_identity_mismatch", "GitHub scope differs from its repository registration")
115
+ branch = reader.get_json(f"repos/{registration.full_name}/branches/main")
116
+ if not isinstance(branch, Mapping) or branch.get("name") != "main" or not isinstance(branch.get("commit"), Mapping):
117
+ raise HostedError("invalid_github_identity", "GitHub did not return the registered main branch")
118
+ head = enforce_expected_sha(branch["commit"].get("sha"), expected, field="baseline_expected_sha",
119
+ mismatch_code="expected_baseline_mismatch")
120
+ commit = reader.get_json(f"repos/{registration.full_name}/git/commits/{head}")
121
+ if (not isinstance(commit, Mapping) or commit.get("sha") != head or
122
+ not isinstance(commit.get("tree"), Mapping)):
123
+ raise HostedError("invalid_github_identity", "GitHub commit differs from the frozen main revision")
124
+ tree = require_commit_sha(commit["tree"].get("sha"), field="tree_sha")
125
+ return ResolvedMainRevision(registration.full_name, registration.repository_id, team_id, head, tree,
126
+ datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), expected)
@@ -0,0 +1,105 @@
1
+ """Fail-closed Linux gVisor execution. No local/subprocess fallback for PR code.
2
+
3
+ The trusted host stages declared files, invokes Docker with a verified runsc
4
+ runtime, and captures bounded streams. The guest sees only source, two protocol
5
+ helper modules, and one label-free request. No SDK fixture/grader or credentials.
6
+ """
7
+ from __future__ import annotations
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import threading
14
+ import time
15
+ import uuid
16
+ from pathlib import Path
17
+ from .contracts import HostedError
18
+
19
+ MAX_STREAM = 1_048_576
20
+ POLICY = {'id': 'sphynx.gvisor.cpu.v1', 'cpus': 1, 'memory_bytes': 1_073_741_824,
21
+ 'pids': 64, 'tmpfs_bytes': 67_108_864, 'case_timeout_seconds': 30,
22
+ 'build_step_timeout_seconds': 30, 'network': 'none', 'user': '65532:65532',
23
+ 'root_filesystem': 'read_only', 'fresh_process_per_case': True}
24
+
25
+ class GVisorSandbox:
26
+ def __init__(self, image: str, *, runsc_path='/usr/local/bin/runsc', expected_runsc_sha256: str):
27
+ if os.name != 'posix': raise HostedError('sandbox_unavailable', 'hosted PR execution requires a Linux gVisor worker')
28
+ if not isinstance(image, str) or not re.fullmatch(r'[a-zA-Z0-9./:_-]+@sha256:[0-9a-f]{64}', image):
29
+ raise HostedError('invalid_runtime_identity', 'the candidate image must use an immutable digest')
30
+ if not re.fullmatch('[0-9a-f]{64}', expected_runsc_sha256):
31
+ raise HostedError('invalid_runtime_identity', 'a trusted runsc binary digest is required')
32
+ self.image, self.runsc_path, self.runsc_hash = image, Path(runsc_path), expected_runsc_sha256
33
+ self._verified = False
34
+ def verify(self):
35
+ if self._verified: return self.identity()
36
+ if hashlib.sha256(self.runsc_path.read_bytes()).hexdigest() != self.runsc_hash:
37
+ raise HostedError('sandbox_identity_mismatch', 'runsc binary differs from the approved runtime')
38
+ result = subprocess.run(['docker', 'info', '--format', '{{json .Runtimes}}'], capture_output=True, timeout=20, check=True)
39
+ runtimes = json.loads(result.stdout)
40
+ configured = runtimes.get('runsc', {}).get('path')
41
+ if not isinstance(configured, str) or Path(configured).resolve() != self.runsc_path.resolve():
42
+ raise HostedError('sandbox_unavailable', 'Docker is not configured with the approved runsc runtime')
43
+ subprocess.run(['docker', 'image', 'inspect', self.image], capture_output=True, timeout=20, check=True)
44
+ self._verified = True
45
+ return self.identity()
46
+ def identity(self):
47
+ return {'image_digest': self.image, 'runsc_sha256': self.runsc_hash, 'resource_policy': dict(POLICY)}
48
+ def command(self, argv, source: Path, helper: Path, name: str):
49
+ return ['docker', 'run', '--rm', '--name', name, '--runtime=runsc', '--network=none',
50
+ '--read-only', '--cap-drop=ALL', '--security-opt=no-new-privileges:true',
51
+ '--cpus=1', '--memory=1g', '--memory-swap=1g', '--pids-limit=64', '--ulimit=nofile=128:128',
52
+ '--user=65532:65532', '--log-driver=none',
53
+ '--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=64m,uid=65532,gid=65532,mode=700',
54
+ '--mount', 'type=bind,source=' + str(source.resolve()) + ',target=/candidate,readonly',
55
+ '--mount', 'type=bind,source=' + str(helper.resolve()) + ',target=/helper,readonly',
56
+ '--workdir=/candidate', '--env=PYTHONPATH=/helper:/candidate', '--env=PYTHONPYCACHEPREFIX=/tmp/cache',
57
+ '--env=HOME=/tmp', '--env=PYTHONUTF8=1', '--env=PYTHONDONTWRITEBYTECODE=1',
58
+ '-i', self.image, *argv]
59
+ def execute(self, argv, source, helper, payload: bytes, *, timeout: float = 30):
60
+ self.verify()
61
+ name = 'sphynx-' + uuid.uuid4().hex
62
+ command = self.command(argv, source, helper, name)
63
+ start = time.monotonic()
64
+ process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
65
+ env={key: value for key, value in os.environ.items() if key in ('PATH', 'LANG')})
66
+ streams = {'stdout': bytearray(), 'stderr': bytearray()}
67
+ exceeded = threading.Event()
68
+ def read(stream, label):
69
+ try:
70
+ while block := stream.read(8192):
71
+ remaining = MAX_STREAM - len(streams[label])
72
+ streams[label].extend(block[:remaining])
73
+ if len(block) > remaining: exceeded.set(); process.kill(); break
74
+ finally: stream.close()
75
+ threads = [threading.Thread(target=read, args=(process.stdout, 'stdout'), daemon=True),
76
+ threading.Thread(target=read, args=(process.stderr, 'stderr'), daemon=True)]
77
+ for thread in threads: thread.start()
78
+ def write_input():
79
+ try:
80
+ process.stdin.write(payload)
81
+ except (BrokenPipeError, OSError):
82
+ pass
83
+ finally:
84
+ try: process.stdin.close()
85
+ except OSError: pass
86
+ writer = threading.Thread(target=write_input, daemon=True)
87
+ writer.start()
88
+ status = 'ok'
89
+ try:
90
+ process.wait(timeout=timeout)
91
+ except subprocess.TimeoutExpired:
92
+ status = 'timeout'; process.kill(); process.wait(timeout=5)
93
+ finally:
94
+ ended = time.monotonic()
95
+ # CLI termination alone would leave the guest running. Remove it on the host.
96
+ subprocess.run(['docker', 'rm', '-f', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20)
97
+ writer.join(5)
98
+ for thread in threads: thread.join(5)
99
+ if exceeded.is_set(): status = 'output_limit_exceeded'
100
+ elif status == 'ok' and process.returncode != 0:
101
+ if process.returncode == 125: raise HostedError('sandbox_launch_failed', 'the trusted sandbox could not start')
102
+ status = 'process_failed'
103
+ return {'status': status, 'stdout_base64': __import__('base64').b64encode(streams['stdout']).decode(),
104
+ 'stderr_base64': __import__('base64').b64encode(streams['stderr']).decode(),
105
+ 'exit_code': process.returncode, 'runner_latency_ms': (ended - start) * 1000}
@@ -0,0 +1,287 @@
1
+ """Resolve one registered PR and freeze declared source files at its head SHA.
2
+
3
+ This is trusted submission preparation, not a build service or evaluation runner.
4
+ It never checks out a branch, follows a PR merge ref, or executes repository code.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import base64
9
+ import hashlib
10
+ import re
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Callable, Mapping, Protocol
14
+ from urllib.parse import urlsplit
15
+
16
+ from ..contracts import INTERFACE_VERSION, canonical_json
17
+ from ..metrics import GRADER_RELEASE, METRIC_RELEASE
18
+ from ..rag import FIXTURE_SHA256
19
+ from .contracts import (CandidateConfig, HostedError, MAX_CONFIG_BYTES, MAX_FILE_BYTES,
20
+ MAX_SOURCE_BYTES, FAMILY_SUBMISSION_SCHEMA, SelectedCandidateRepository,
21
+ candidate_method_id, parse_candidate_repository)
22
+ from .registry import BASELINE_ID, SUITE_ID, HostedRegistry, repository_name
23
+ from .revisions import enforce_expected_sha, require_commit_sha
24
+
25
+ SUBMISSION_SCHEMA = "sphynx.hosted.prepared-submission.v1"
26
+ _SHA = re.compile(r"^[0-9a-f]{40}$")
27
+ _PR = re.compile(r"^/([A-Za-z0-9][A-Za-z0-9-]*)/([A-Za-z0-9][A-Za-z0-9_.-]*)/pull/([1-9][0-9]*)/?$")
28
+
29
+
30
+ class GitHubReader(Protocol):
31
+ """Implemented by a trusted GitHub App transport in the hosted API later."""
32
+ def get_json(self, path: str) -> Mapping[str, Any]: ...
33
+
34
+
35
+ def pr_identity(url: object) -> tuple[str, str, int]:
36
+ if not isinstance(url, str) or len(url) > 512:
37
+ raise HostedError("invalid_pr_url", "use an HTTPS github.com repository/pull/number URL")
38
+ if any(character.isspace() or ord(character) < 32 for character in url):
39
+ raise HostedError("invalid_pr_url", "PR URL cannot contain whitespace or control characters")
40
+ try:
41
+ parsed = urlsplit(url)
42
+ except ValueError:
43
+ raise HostedError("invalid_pr_url", "use an HTTPS github.com repository/pull/number URL") from None
44
+ match = _PR.fullmatch(parsed.path)
45
+ if parsed.scheme != "https" or parsed.netloc != "github.com" or parsed.query or parsed.fragment or match is None:
46
+ raise HostedError("invalid_pr_url", "use an HTTPS github.com repository/pull/number URL")
47
+ full_name = repository_name(match.group(1) + "/" + match.group(2))
48
+ number = int(match.group(3))
49
+ return f"https://github.com/{full_name}/pull/{number}", full_name, number
50
+
51
+
52
+ def _sha(value: object) -> str:
53
+ if not isinstance(value, str) or not _SHA.fullmatch(value):
54
+ raise HostedError("invalid_github_identity", "GitHub must return a complete commit or object SHA")
55
+ return value
56
+
57
+
58
+ def _mapping(value: object) -> Mapping[str, Any]:
59
+ if not isinstance(value, Mapping):
60
+ raise HostedError("invalid_github_response", "GitHub returned an unexpected object")
61
+ return value
62
+
63
+
64
+ def _repo(value: object) -> tuple[str, int]:
65
+ value = _mapping(value)
66
+ name = repository_name(value.get("full_name"))
67
+ repo_id = value.get("id")
68
+ if type(repo_id) is not int or repo_id <= 0:
69
+ raise HostedError("invalid_github_identity", "GitHub repository ID is missing")
70
+ return name, repo_id
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ResolvedPR:
75
+ pr_url: str
76
+ pr_number: int
77
+ base_repository: str
78
+ base_repository_id: int
79
+ head_repository: str
80
+ head_repository_id: int
81
+ head_sha: str
82
+ team_id: str
83
+ resolved_at_utc: str
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ return dict(vars(self))
87
+
88
+ @classmethod
89
+ def from_dict(cls, value: Mapping[str, Any]) -> "ResolvedPR":
90
+ expected = {"pr_url", "pr_number", "base_repository", "base_repository_id",
91
+ "head_repository", "head_repository_id", "head_sha", "team_id", "resolved_at_utc"}
92
+ if not isinstance(value, Mapping) or set(value) != expected:
93
+ raise HostedError("invalid_recorded_head", "recorded PR head has an unsupported shape")
94
+ return cls(**value)
95
+
96
+
97
+ def resolve_pull_request(reader: GitHubReader, registry: HostedRegistry, *, team_id: str,
98
+ candidate_pr: str, suite: str = SUITE_ID, baseline: str = BASELINE_ID,
99
+ expected_head_sha: str | None = None) -> ResolvedPR:
100
+ if expected_head_sha is not None:
101
+ expected_head_sha = require_commit_sha(expected_head_sha, field='expected_head_sha')
102
+ pr_url, requested_repo, number = pr_identity(candidate_pr)
103
+ registry.authorize(team_id, requested_repo, suite, baseline)
104
+ pr = _mapping(reader.get_json(f"repos/{requested_repo}/pulls/{number}"))
105
+ if type(pr.get("number")) is not int or pr["number"] != number:
106
+ raise HostedError("invalid_github_identity", "GitHub PR number does not match the submission")
107
+ base_name, base_id = _repo(_mapping(pr.get("base")).get("repo"))
108
+ if base_name.casefold() != requested_repo.casefold():
109
+ raise HostedError("repository_identity_mismatch", "GitHub PR base differs from the requested repository")
110
+ registry.authorize(team_id, base_name, suite, baseline, repository_id=base_id)
111
+ head = _mapping(pr.get("head"))
112
+ if head.get("repo") is None:
113
+ raise HostedError("head_repository_unavailable", "PR head repository has been deleted or is inaccessible")
114
+ head_name, head_id = _repo(head["repo"])
115
+ registry.authorize(team_id, head_name, suite, baseline, repository_id=head_id)
116
+ head_sha = _sha(head.get("sha"))
117
+ enforce_expected_sha(head_sha, expected_head_sha)
118
+ return ResolvedPR(pr_url, number, base_name, base_id, head_name, head_id, head_sha,
119
+ team_id, datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"))
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class SourceFile:
124
+ path: str
125
+ content: bytes
126
+ git_blob_sha: str
127
+
128
+ @property
129
+ def digest(self) -> str:
130
+ return "sha256:" + hashlib.sha256(self.content).hexdigest()
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class FrozenSubmission:
135
+ pr_url: str
136
+ pr_number: int
137
+ base_repository: str
138
+ base_repository_id: int
139
+ head_repository: str
140
+ head_repository_id: int
141
+ head_sha: str
142
+ tree_sha: str
143
+ team_id: str
144
+ config: CandidateConfig
145
+ files: tuple[SourceFile, ...]
146
+ resolved_at_utc: str
147
+ repository_selection: SelectedCandidateRepository | None = None
148
+ model_runtime_json: str | None = None
149
+
150
+ @property
151
+ def source_bundle_digest(self) -> str:
152
+ identity = {
153
+ "head_repository_id": self.head_repository_id, "head_sha": self.head_sha,
154
+ "files": [{"path": source.path, "sha256": source.digest} for source in self.files],
155
+ }
156
+ return "sha256:" + hashlib.sha256(canonical_json(identity).encode("utf-8")).hexdigest()
157
+
158
+ def to_manifest(self) -> dict[str, Any]:
159
+ manifest = {
160
+ "schema_version": SUBMISSION_SCHEMA, "status": "prepared",
161
+ "hosted_execution_verified": False, "team_id": self.team_id,
162
+ "candidate_pr": self.pr_url, "pr_number": self.pr_number,
163
+ "base_repository": {"id": self.base_repository_id, "full_name": self.base_repository},
164
+ "head_repository": {"id": self.head_repository_id, "full_name": self.head_repository},
165
+ "head_sha": self.head_sha, "tree_sha": self.tree_sha,
166
+ "resolved_at_utc": self.resolved_at_utc,
167
+ "candidate_configuration": self.config.to_dict(),
168
+ "configuration_digest": self.config.digest,
169
+ "source_bundle_digest": self.source_bundle_digest,
170
+ "files": [{"path": source.path, "size_bytes": len(source.content),
171
+ "git_blob_sha": source.git_blob_sha, "sha256": source.digest} for source in self.files],
172
+ "suite": {"id": SUITE_ID, "dataset_digest": "sha256:" + FIXTURE_SHA256,
173
+ "case_count": 6, "interface_version": INTERFACE_VERSION,
174
+ "grader_release": GRADER_RELEASE, "metric_release": METRIC_RELEASE},
175
+ "baseline": {"id": BASELINE_ID, "score_source": "supplied_dense_scores",
176
+ "fresh_model_inference": False},
177
+ "requested_runtime_profile": self.config.runtime_profile,
178
+ "runtime_identity": None, "metrics": None, "report_location": None,
179
+ }
180
+ if self.repository_selection is not None and self.repository_selection.family is not None:
181
+ manifest.update(schema_version=FAMILY_SUBMISSION_SCHEMA,
182
+ candidate_method=self.repository_selection.candidate_method,
183
+ candidate_family=self.repository_selection.family_selection())
184
+ if self.model_runtime_json is not None:
185
+ from .model_contracts import MODEL_SUBMISSION_SCHEMA
186
+ import json
187
+ manifest.update(schema_version=MODEL_SUBMISSION_SCHEMA,
188
+ model_runtime=json.loads(self.model_runtime_json))
189
+ return manifest
190
+
191
+
192
+ def prepare_submission(reader: GitHubReader, registry: HostedRegistry, *, team_id: str,
193
+ candidate_pr: str, suite: str = SUITE_ID, baseline: str = BASELINE_ID,
194
+ pinned: ResolvedPR | None = None,
195
+ on_resolved: Callable[[ResolvedPR], None] | None = None,
196
+ expected_head_sha: str | None = None,
197
+ candidate_method: str | None = None,
198
+ model_runtime=None) -> FrozenSubmission:
199
+ if candidate_method is not None:
200
+ candidate_method = candidate_method_id(candidate_method)
201
+ if expected_head_sha is not None:
202
+ expected_head_sha = require_commit_sha(expected_head_sha, field='expected_head_sha')
203
+ pr_url, requested_repo, number = pr_identity(candidate_pr)
204
+ registry.authorize(team_id, requested_repo, suite, baseline)
205
+ resolved = pinned or resolve_pull_request(reader, registry, team_id=team_id,
206
+ candidate_pr=candidate_pr, suite=suite, baseline=baseline,
207
+ expected_head_sha=expected_head_sha)
208
+ if (resolved.pr_url != pr_url or type(resolved.pr_number) is not int or resolved.pr_number != number
209
+ or resolved.base_repository.casefold() != requested_repo.casefold() or resolved.team_id != team_id):
210
+ raise HostedError("recorded_head_mismatch", "recorded head belongs to another submission")
211
+ for name, repo_id in ((resolved.base_repository, resolved.base_repository_id),
212
+ (resolved.head_repository, resolved.head_repository_id)):
213
+ if type(repo_id) is not int or repo_id <= 0:
214
+ raise HostedError("invalid_recorded_head", "recorded repository ID is invalid")
215
+ registry.authorize(team_id, repository_name(name), suite, baseline, repository_id=repo_id)
216
+ commit_sha = _sha(resolved.head_sha)
217
+ enforce_expected_sha(commit_sha, expected_head_sha)
218
+ if on_resolved is not None:
219
+ on_resolved(resolved) # Persist before source fetch; retries resume this exact head.
220
+ base_name, base_id = resolved.base_repository, resolved.base_repository_id
221
+ head_name, head_id = resolved.head_repository, resolved.head_repository_id
222
+ prefix = f"repos/{head_name}/git"
223
+ commit = _mapping(reader.get_json(f"{prefix}/commits/{commit_sha}"))
224
+ if commit.get("sha") != commit_sha:
225
+ raise HostedError("commit_identity_mismatch", "GitHub returned a different commit")
226
+ tree_sha = _sha(_mapping(commit.get("tree")).get("sha"))
227
+ trees: dict[str, Mapping[str, Any]] = {}
228
+
229
+ def read_file(path: str, maximum: int) -> SourceFile:
230
+ current_sha = tree_sha
231
+ components = path.split("/")
232
+ for index, component in enumerate(components):
233
+ if current_sha not in trees:
234
+ tree = _mapping(reader.get_json(f"{prefix}/trees/{current_sha}"))
235
+ entries = tree.get("tree")
236
+ if tree.get("sha") != current_sha or tree.get("truncated") or not isinstance(entries, list) or len(entries) > 10_000:
237
+ raise HostedError("invalid_source_tree", "source tree is truncated, oversized, or has a different identity")
238
+ trees[current_sha] = tree
239
+ matches = [item for item in trees[current_sha]["tree"]
240
+ if isinstance(item, Mapping) and item.get("path") == component]
241
+ if len(matches) != 1:
242
+ code = "candidate_config_missing" if path == "sphynx.toml" else "source_file_missing"
243
+ raise HostedError(code, "the pinned commit does not contain a declared regular file")
244
+ entry = matches[0]
245
+ current_sha = _sha(entry.get("sha"))
246
+ final = index == len(components) - 1
247
+ if not final:
248
+ if entry.get("type") != "tree" or entry.get("mode") != "040000":
249
+ raise HostedError("unsafe_source_entry", "source directory cannot be a symlink or submodule")
250
+ elif entry.get("type") != "blob" or entry.get("mode") not in ("100644", "100755"):
251
+ raise HostedError("unsafe_source_entry", "source files cannot be symlinks or submodules")
252
+ blob = _mapping(reader.get_json(f"{prefix}/blobs/{current_sha}"))
253
+ content, size = blob.get("content"), blob.get("size")
254
+ if blob.get("sha") != current_sha or blob.get("encoding") != "base64" or not isinstance(content, str):
255
+ raise HostedError("invalid_source_blob", "GitHub did not return the requested encoded blob")
256
+ if type(size) is not int or size < 0 or size > maximum or len(content) > maximum * 2 + 1024:
257
+ raise HostedError("source_size_limit", "source file exceeds the pilot size limit")
258
+ try:
259
+ data = base64.b64decode("".join(content.splitlines()), validate=True)
260
+ except (ValueError, UnicodeEncodeError):
261
+ raise HostedError("invalid_source_blob", "source blob is not valid base64") from None
262
+ actual_sha = hashlib.sha1(b"blob " + str(len(data)).encode("ascii") + b"\0" + data).hexdigest()
263
+ if len(data) != size or actual_sha != current_sha:
264
+ raise HostedError("source_digest_mismatch", "source bytes do not match the pinned Git object")
265
+ return SourceFile(path, data, current_sha)
266
+
267
+ configuration_file = read_file("sphynx.toml", MAX_CONFIG_BYTES)
268
+ selection = parse_candidate_repository(configuration_file.content, candidate_method=candidate_method)
269
+ config = selection.config
270
+ model_runtime_json = None
271
+ if config.model_kind == "registered":
272
+ from .model_admission import ModelRuntimeAdmission, require_model_authorization
273
+ if model_runtime is None:
274
+ raise HostedError("models_not_admitted", "this candidate requires an operator-verified model runtime admission")
275
+ record = model_runtime.to_dict() if isinstance(model_runtime, ModelRuntimeAdmission) else model_runtime
276
+ admitted = require_model_authorization(record, team_id=team_id, base_repository_id=base_id,
277
+ head_repository_id=head_id, suite=suite, baseline=baseline)
278
+ model_runtime_json = canonical_json(admitted)
279
+ sources = [configuration_file]
280
+ for path in sorted(config.source_files):
281
+ sources.append(read_file(path, MAX_FILE_BYTES))
282
+ if sum(len(source.content) for source in sources) > MAX_SOURCE_BYTES:
283
+ raise HostedError("source_size_limit", "candidate bundle exceeds the pilot size limit")
284
+ return FrozenSubmission(pr_url, number, base_name, base_id, head_name, head_id, commit_sha,
285
+ tree_sha, team_id, config, tuple(sources),
286
+ resolved.resolved_at_utc, selection if selection.family is not None else None,
287
+ model_runtime_json)