fix(diarization): связана переносимая проверка handoff
Зачем: - капсула должна содержать каждый вход, названный в manifest - Intel handoff нужно проверять на Ryzen без сравнения чужого SKU Что: - входы manifest связаны с индексом и SHA-256 файлов капсулы - validate-only финального handoff пропускает только проверку машины - версии Python, пакетов и экспериментальной сборки по-прежнему обязательны Проверка: - uv run pytest -q tests/test_benchmark_experiment.py - uv run ruff check для изменённых файлов
This commit is contained in:
1 parent
0ea4d56c9d
commit
6345cb6449
2 files changed
+113
-5
No files matched your search
@@ -879,7 +879,10 @@ def environment_snapshot() -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def validate_environment(
|
def validate_environment(
|
||||||
manifest: Mapping[str, Any], environment: Mapping[str, Any]
|
manifest: Mapping[str, Any],
|
||||||
|
environment: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
check_machine: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
experiment = manifest["experiment"]
|
experiment = manifest["experiment"]
|
||||||
for name, expected in experiment["dependencies"].items():
|
for name, expected in experiment["dependencies"].items():
|
||||||
@@ -898,6 +901,8 @@ def validate_environment(
|
|||||||
}
|
}
|
||||||
if expected_builds != {runtime_build}:
|
if expected_builds != {runtime_build}:
|
||||||
raise ValueError("Хеш runtime-сборки не совпал с enabled-ячейками")
|
raise ValueError("Хеш runtime-сборки не совпал с enabled-ячейками")
|
||||||
|
if not check_machine:
|
||||||
|
return
|
||||||
machine = manifest["machine"]
|
machine = manifest["machine"]
|
||||||
if environment.get("platform") != machine["os"]:
|
if environment.get("platform") != machine["os"]:
|
||||||
raise ValueError("ОС не совпала с manifest")
|
raise ValueError("ОС не совпала с manifest")
|
||||||
@@ -946,7 +951,9 @@ def _runtime_profile() -> str | None:
|
|||||||
except (OSError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
return None
|
return None
|
||||||
value = completed.stdout.strip()
|
value = completed.stdout.strip()
|
||||||
return value or None if completed.returncode == 0 else None
|
if completed.returncode != 0:
|
||||||
|
return None
|
||||||
|
return value or None
|
||||||
|
|
||||||
|
|
||||||
def _runtime_governor() -> str | None:
|
def _runtime_governor() -> str | None:
|
||||||
@@ -1640,6 +1647,32 @@ def worker_main(request_path: Path) -> None:
|
|||||||
_save_json(Path(request["response_path"]), result)
|
_save_json(Path(request["response_path"]), result)
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_file_index(manifest: Mapping[str, Any]) -> dict[str, str]:
|
||||||
|
"""Возвращает каждый входной файл, который manifest требует от капсулы."""
|
||||||
|
required = {}
|
||||||
|
|
||||||
|
def add(item: Mapping[str, Any], label: str) -> None:
|
||||||
|
path = _require_relative_path(item.get("path"), f"{label}.path")
|
||||||
|
sha256 = _require_sha256(item.get("sha256"), f"{label}.sha256")
|
||||||
|
previous = required.setdefault(path, sha256)
|
||||||
|
if previous != sha256:
|
||||||
|
raise ValueError(f"{label}: один путь ссылается на разные SHA-256")
|
||||||
|
|
||||||
|
for artifact in manifest.get("artifacts", []):
|
||||||
|
add(artifact, f"artifact:{artifact.get('id')}")
|
||||||
|
for recording in manifest.get("recordings", []):
|
||||||
|
label = f"recording:{recording.get('id')}"
|
||||||
|
add(recording, label)
|
||||||
|
for key in ("reference", "asr_words"):
|
||||||
|
if (side := recording.get(key)) is not None:
|
||||||
|
add(side, f"{label}.{key}")
|
||||||
|
for source in manifest.get("calibration", []):
|
||||||
|
add(source, f"calibration:{source.get('id')}")
|
||||||
|
if not required:
|
||||||
|
raise ValueError("Manifest капсулы не содержит входных файлов")
|
||||||
|
return required
|
||||||
|
|
||||||
|
|
||||||
def verify_capsule(
|
def verify_capsule(
|
||||||
archive_path: Path,
|
archive_path: Path,
|
||||||
expected_sha256: str,
|
expected_sha256: str,
|
||||||
@@ -1708,6 +1741,11 @@ def verify_capsule(
|
|||||||
if declared["manifest.json"] != manifest_sha256:
|
if declared["manifest.json"] != manifest_sha256:
|
||||||
raise ValueError("Хеш manifest.json не совпал с индексом капсулы")
|
raise ValueError("Хеш manifest.json не совпал с индексом капсулы")
|
||||||
archived_manifest_value = json.loads(archived_manifest)
|
archived_manifest_value = json.loads(archived_manifest)
|
||||||
|
for name, expected in _manifest_file_index(archived_manifest_value).items():
|
||||||
|
if declared.get(name) != expected:
|
||||||
|
raise ValueError(
|
||||||
|
"Входной файл manifest не совпал с индексом файлов капсулы"
|
||||||
|
)
|
||||||
handoff = archived_manifest_value.get("handoff", {})
|
handoff = archived_manifest_value.get("handoff", {})
|
||||||
for candidate in handoff.get("candidate_recipes", []):
|
for candidate in handoff.get("candidate_recipes", []):
|
||||||
result_file = candidate.get("result_file")
|
result_file = candidate.get("result_file")
|
||||||
@@ -1738,8 +1776,6 @@ def run_cli(args: Any, raw_manifest: Mapping[str, Any]) -> None:
|
|||||||
)
|
)
|
||||||
manifest = resolve_manifest_paths(raw_manifest, args.manifest.parent)
|
manifest = resolve_manifest_paths(raw_manifest, args.manifest.parent)
|
||||||
warnings = validate_manifest(manifest)
|
warnings = validate_manifest(manifest)
|
||||||
environment = environment_snapshot()
|
|
||||||
validate_environment(manifest, environment)
|
|
||||||
capsule_path = getattr(args, "capsule", None)
|
capsule_path = getattr(args, "capsule", None)
|
||||||
capsule_sha256 = getattr(args, "capsule_sha256", None)
|
capsule_sha256 = getattr(args, "capsule_sha256", None)
|
||||||
finalized_handoff = manifest["handoff"]["outcome"] != "pending"
|
finalized_handoff = manifest["handoff"]["outcome"] != "pending"
|
||||||
@@ -1755,6 +1791,15 @@ def run_cli(args: Any, raw_manifest: Mapping[str, Any]) -> None:
|
|||||||
expected_report_sha256=manifest["handoff"]["public_report_sha256"],
|
expected_report_sha256=manifest["handoff"]["public_report_sha256"],
|
||||||
expected_capsule_id=manifest["handoff"]["capsule_id"],
|
expected_capsule_id=manifest["handoff"]["capsule_id"],
|
||||||
)
|
)
|
||||||
|
environment = environment_snapshot()
|
||||||
|
portable_handoff_check = (
|
||||||
|
args.validate_only and finalized_handoff and capsule_result is not None
|
||||||
|
)
|
||||||
|
validate_environment(
|
||||||
|
manifest,
|
||||||
|
environment,
|
||||||
|
check_machine=not portable_handoff_check,
|
||||||
|
)
|
||||||
if args.validate_only:
|
if args.validate_only:
|
||||||
plan = safe_plan(manifest, warnings, environment)
|
plan = safe_plan(manifest, warnings, environment)
|
||||||
if capsule_result is not None:
|
if capsule_result is not None:
|
||||||
|
|||||||
@@ -294,6 +294,34 @@ def test_identity_and_runtime_validation_cover_exact_versions_and_build(tmp_path
|
|||||||
experiment.validate_environment(manifest, changed_build)
|
experiment.validate_environment(manifest, changed_build)
|
||||||
|
|
||||||
|
|
||||||
|
def test_portable_handoff_check_skips_only_machine_identity(tmp_path):
|
||||||
|
manifest = _manifest(tmp_path)
|
||||||
|
foreign_environment = copy.deepcopy(_environment())
|
||||||
|
foreign_environment.update(
|
||||||
|
platform="Linux Ryzen",
|
||||||
|
logical_cores=24,
|
||||||
|
physical_cores=12,
|
||||||
|
cpu_sku="AMD Ryzen 9 5900X 12-Core Processor",
|
||||||
|
cpu_flags=["avx", "avx2", "avx512f"],
|
||||||
|
ram_bytes=64 * 1024**3,
|
||||||
|
swap_total_bytes=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
experiment.validate_environment(
|
||||||
|
manifest,
|
||||||
|
foreign_environment,
|
||||||
|
check_machine=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
foreign_environment["packages"]["numpy"] = "2.4.4"
|
||||||
|
with pytest.raises(ValueError, match="numpy==2.4.3"):
|
||||||
|
experiment.validate_environment(
|
||||||
|
manifest,
|
||||||
|
foreign_environment,
|
||||||
|
check_machine=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_manifest_rejects_stage_boundary_thread_budget_and_non_alternating_ab(tmp_path):
|
def test_manifest_rejects_stage_boundary_thread_budget_and_non_alternating_ab(tmp_path):
|
||||||
manifest = _manifest(tmp_path)
|
manifest = _manifest(tmp_path)
|
||||||
manifest["cells"][0]["stage"] = "ryzen"
|
manifest["cells"][0]["stage"] = "ryzen"
|
||||||
@@ -733,7 +761,19 @@ def test_capsule_verification_checks_hashes_commits_and_safe_paths(tmp_path):
|
|||||||
payload = b"private"
|
payload = b"private"
|
||||||
payload_hash = hashlib.sha256(payload).hexdigest()
|
payload_hash = hashlib.sha256(payload).hexdigest()
|
||||||
manifest_path = tmp_path / "manifest.json"
|
manifest_path = tmp_path / "manifest.json"
|
||||||
manifest_bytes = b'{"schema_version":3}\n'
|
manifest_value = {
|
||||||
|
"schema_version": 3,
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"id": "input",
|
||||||
|
"path": "data/payload.bin",
|
||||||
|
"sha256": payload_hash,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recordings": [],
|
||||||
|
"calibration": [],
|
||||||
|
}
|
||||||
|
manifest_bytes = json.dumps(manifest_value).encode()
|
||||||
manifest_path.write_bytes(manifest_bytes)
|
manifest_path.write_bytes(manifest_bytes)
|
||||||
capsule = {
|
capsule = {
|
||||||
"capsule_id": "opaque-41",
|
"capsule_id": "opaque-41",
|
||||||
@@ -768,6 +808,19 @@ def test_capsule_verification_checks_hashes_commits_and_safe_paths(tmp_path):
|
|||||||
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
|
"manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
incomplete_capsule = copy.deepcopy(capsule)
|
||||||
|
incomplete_capsule["files"].pop("data/payload.bin")
|
||||||
|
incomplete_path = tmp_path / "incomplete.zip"
|
||||||
|
with zipfile.ZipFile(incomplete_path, "w") as archive:
|
||||||
|
archive.writestr("capsule.json", json.dumps(incomplete_capsule))
|
||||||
|
archive.writestr("manifest.json", manifest_bytes)
|
||||||
|
with pytest.raises(ValueError, match="Входной файл manifest"):
|
||||||
|
experiment.verify_capsule(
|
||||||
|
incomplete_path,
|
||||||
|
experiment.file_sha256(incomplete_path),
|
||||||
|
expected_manifest_path=manifest_path,
|
||||||
|
)
|
||||||
|
|
||||||
unrelated_manifest = tmp_path / "unrelated-manifest.json"
|
unrelated_manifest = tmp_path / "unrelated-manifest.json"
|
||||||
unrelated_manifest.write_bytes(b'{"schema_version":3,"other":true}\n')
|
unrelated_manifest.write_bytes(b'{"schema_version":3,"other":true}\n')
|
||||||
with pytest.raises(ValueError, match="не совпал с manifest.json капсулы"):
|
with pytest.raises(ValueError, match="не совпал с manifest.json капсулы"):
|
||||||
@@ -791,9 +844,16 @@ def test_capsule_verification_checks_hashes_commits_and_safe_paths(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_capsule_binds_candidate_result_evidence_to_file_index(tmp_path):
|
def test_capsule_binds_candidate_result_evidence_to_file_index(tmp_path):
|
||||||
|
input_bytes = b"input"
|
||||||
|
input_hash = hashlib.sha256(input_bytes).hexdigest()
|
||||||
result_bytes = b'{"aggregate":{"quality_passed":true}}'
|
result_bytes = b'{"aggregate":{"quality_passed":true}}'
|
||||||
result_hash = hashlib.sha256(result_bytes).hexdigest()
|
result_hash = hashlib.sha256(result_bytes).hexdigest()
|
||||||
manifest_value = {
|
manifest_value = {
|
||||||
|
"artifacts": [
|
||||||
|
{"id": "input", "path": "data/input.bin", "sha256": input_hash}
|
||||||
|
],
|
||||||
|
"recordings": [],
|
||||||
|
"calibration": [],
|
||||||
"handoff": {
|
"handoff": {
|
||||||
"candidate_recipes": [
|
"candidate_recipes": [
|
||||||
{
|
{
|
||||||
@@ -812,6 +872,7 @@ def test_capsule_binds_candidate_result_evidence_to_file_index(tmp_path):
|
|||||||
"public_report_sha256": "4" * 64,
|
"public_report_sha256": "4" * 64,
|
||||||
"files": {
|
"files": {
|
||||||
"manifest.json": hashlib.sha256(manifest_bytes).hexdigest(),
|
"manifest.json": hashlib.sha256(manifest_bytes).hexdigest(),
|
||||||
|
"data/input.bin": input_hash,
|
||||||
"results/candidate.json": result_hash,
|
"results/candidate.json": result_hash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -819,6 +880,7 @@ def test_capsule_binds_candidate_result_evidence_to_file_index(tmp_path):
|
|||||||
with zipfile.ZipFile(archive_path, "w") as archive:
|
with zipfile.ZipFile(archive_path, "w") as archive:
|
||||||
archive.writestr("capsule.json", json.dumps(capsule))
|
archive.writestr("capsule.json", json.dumps(capsule))
|
||||||
archive.writestr("manifest.json", manifest_bytes)
|
archive.writestr("manifest.json", manifest_bytes)
|
||||||
|
archive.writestr("data/input.bin", input_bytes)
|
||||||
archive.writestr("results/candidate.json", result_bytes)
|
archive.writestr("results/candidate.json", result_bytes)
|
||||||
|
|
||||||
experiment.verify_capsule(
|
experiment.verify_capsule(
|
||||||
@@ -843,6 +905,7 @@ def test_capsule_binds_candidate_result_evidence_to_file_index(tmp_path):
|
|||||||
with zipfile.ZipFile(broken_path, "w") as archive:
|
with zipfile.ZipFile(broken_path, "w") as archive:
|
||||||
archive.writestr("capsule.json", json.dumps(capsule))
|
archive.writestr("capsule.json", json.dumps(capsule))
|
||||||
archive.writestr("manifest.json", broken_manifest_bytes)
|
archive.writestr("manifest.json", broken_manifest_bytes)
|
||||||
|
archive.writestr("data/input.bin", input_bytes)
|
||||||
archive.writestr("results/candidate.json", result_bytes)
|
archive.writestr("results/candidate.json", result_bytes)
|
||||||
with pytest.raises(ValueError, match="Результат кандидата"):
|
with pytest.raises(ValueError, match="Результат кандидата"):
|
||||||
experiment.verify_capsule(
|
experiment.verify_capsule(
|
||||||
|
|||||||
Reference in new issue
Block a user