fix: canonicalize provider review asset hashes
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 9m31s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 19m12s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 5m5s
Build and Test Quality Gate / Contract Web typecheck and build (pull_request) Successful in 9m31s
Build and Test Quality Gate / Python tests and Contract API migration (pull_request) Failing after 19m12s
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Successful in 5m5s
This commit is contained in:
+2
-1
@@ -12,6 +12,7 @@ from urllib.parse import urlparse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from yuqei_ai_platform_api.provider_legal_approval import SignedProviderLegalApproval
|
||||
from yuqei_ai_platform_api.provider_review_package import canonical_review_asset_bytes
|
||||
|
||||
|
||||
ACCEPTANCE_POLICY_VERSION = "phase13-provider-acceptance-v1"
|
||||
@@ -385,7 +386,7 @@ def _manifest_hash(paths: list[Path]) -> str:
|
||||
for path in paths:
|
||||
digest.update(path.name.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(path.read_bytes())
|
||||
digest.update(canonical_review_asset_bytes(path))
|
||||
digest.update(b"\0")
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
+10
@@ -9,12 +9,14 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
REVIEW_PACKAGE_VERSION = "provider-technical-review-package-v1"
|
||||
CANONICAL_TEXT_ASSET_SUFFIXES = frozenset({".csv", ".json", ".md"})
|
||||
|
||||
|
||||
class StrictModel(BaseModel):
|
||||
@@ -82,6 +84,14 @@ def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def canonical_review_asset_bytes(path: Path) -> bytes:
|
||||
"""Return stable review bytes independent of checkout line endings."""
|
||||
raw = path.read_bytes()
|
||||
if path.suffix.lower() in CANONICAL_TEXT_ASSET_SUFFIXES:
|
||||
return raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
||||
return raw
|
||||
|
||||
|
||||
def _as_utc(value: datetime) -> datetime:
|
||||
if value.tzinfo is None or value.utcoffset() is None:
|
||||
raise ValueError("review package timestamps must be timezone-aware")
|
||||
|
||||
@@ -44,6 +44,7 @@ from yuqei_ai_platform_api.provider_acceptance import expected_acceptance_invent
|
||||
from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
|
||||
ProviderTechnicalReviewPackage,
|
||||
canonical_json,
|
||||
canonical_review_asset_bytes,
|
||||
canonical_review_package,
|
||||
review_package_sha256,
|
||||
sha256_bytes,
|
||||
@@ -170,7 +171,7 @@ def _validate_provider_package_inventory(package: ProviderTechnicalReviewPackage
|
||||
|
||||
expected_assets: dict[str, tuple[str, int]] = {}
|
||||
for asset_path in sorted(item for item in PHASE13_ROOT.rglob("*") if item.is_file()):
|
||||
raw = asset_path.read_bytes()
|
||||
raw = canonical_review_asset_bytes(asset_path)
|
||||
expected_assets[asset_path.relative_to(PHASE13_ROOT).as_posix()] = (
|
||||
sha256_bytes(raw),
|
||||
len(raw),
|
||||
@@ -211,7 +212,7 @@ def _safe_asset(relative: str) -> tuple[str, str, int]:
|
||||
raise ValueError("contract product golden asset escapes its root") from error
|
||||
if not candidate.is_file():
|
||||
raise ValueError("contract product golden asset is not a regular file")
|
||||
raw = candidate.read_bytes()
|
||||
raw = canonical_review_asset_bytes(candidate)
|
||||
return relative.replace("\\", "/"), sha256_bytes(raw), len(raw)
|
||||
|
||||
|
||||
@@ -276,7 +277,7 @@ def build_product_golden_evidence(summary_file: Path) -> dict[str, Any]:
|
||||
}
|
||||
)
|
||||
cases.sort(key=lambda item: (item["suite"], item["sample_id"]))
|
||||
inventory_raw = GENERATION_INVENTORY.read_bytes()
|
||||
inventory_raw = canonical_review_asset_bytes(GENERATION_INVENTORY)
|
||||
return {
|
||||
"summary_file_sha256": sha256_bytes(summary_raw),
|
||||
"inventory_file_sha256": sha256_bytes(inventory_raw),
|
||||
|
||||
@@ -52,6 +52,7 @@ from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
|
||||
ProviderReviewCase,
|
||||
ProviderTechnicalReviewPackage,
|
||||
canonical_json,
|
||||
canonical_review_asset_bytes,
|
||||
canonical_review_package,
|
||||
review_package_sha256,
|
||||
sha256_bytes,
|
||||
@@ -627,7 +628,7 @@ def _manifest_hash(paths: list[Path]) -> str:
|
||||
for path in paths:
|
||||
digest.update(path.name.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(path.read_bytes())
|
||||
digest.update(canonical_review_asset_bytes(path))
|
||||
digest.update(b"\0")
|
||||
return digest.hexdigest()
|
||||
|
||||
@@ -683,7 +684,7 @@ def _review_assets() -> list[ProviderReviewAsset]:
|
||||
assets: list[ProviderReviewAsset] = []
|
||||
for path in sorted(item for item in PHASE13_ROOT.rglob("*") if item.is_file()):
|
||||
relative = path.relative_to(PHASE13_ROOT).as_posix()
|
||||
raw = path.read_bytes()
|
||||
raw = canonical_review_asset_bytes(path)
|
||||
assets.append(
|
||||
ProviderReviewAsset(path=relative, sha256=sha256_bytes(raw), size_bytes=len(raw))
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
|
||||
ProviderReviewCase,
|
||||
ProviderTechnicalReviewPackage,
|
||||
canonical_json,
|
||||
canonical_review_asset_bytes,
|
||||
canonical_review_package,
|
||||
sha256_bytes,
|
||||
)
|
||||
@@ -61,7 +62,7 @@ def _review_package() -> ProviderTechnicalReviewPackage:
|
||||
inventory = expected_acceptance_inventory()
|
||||
assets = []
|
||||
for path in sorted(item for item in helper.PHASE13_ROOT.rglob("*") if item.is_file()):
|
||||
raw = path.read_bytes()
|
||||
raw = canonical_review_asset_bytes(path)
|
||||
assets.append(
|
||||
ProviderReviewAsset(
|
||||
path=path.relative_to(helper.PHASE13_ROOT).as_posix(),
|
||||
|
||||
@@ -36,7 +36,24 @@ from yuqei_ai_platform_api.provider_legal_approval import ( # noqa: E402
|
||||
ProviderLegalApprovalPublicKey,
|
||||
sign_legal_approval,
|
||||
)
|
||||
from yuqei_ai_platform_api.provider_review_package import review_package_sha256 # noqa: E402
|
||||
from yuqei_ai_platform_api.provider_review_package import ( # noqa: E402
|
||||
canonical_review_asset_bytes,
|
||||
review_package_sha256,
|
||||
)
|
||||
|
||||
|
||||
def test_review_asset_and_manifest_hashes_ignore_checkout_line_endings(tmp_path: Path) -> None:
|
||||
lf_dir = tmp_path / "lf"
|
||||
crlf_dir = tmp_path / "crlf"
|
||||
lf_dir.mkdir()
|
||||
crlf_dir.mkdir()
|
||||
lf_path = lf_dir / "inventory.csv"
|
||||
crlf_path = crlf_dir / "inventory.csv"
|
||||
lf_path.write_bytes(b"id,title\n1,alpha\n2,beta\n")
|
||||
crlf_path.write_bytes(b"id,title\r\n1,alpha\r\n2,beta\r\n")
|
||||
|
||||
assert canonical_review_asset_bytes(lf_path) == canonical_review_asset_bytes(crlf_path)
|
||||
assert cli._manifest_hash([lf_path]) == cli._manifest_hash([crlf_path])
|
||||
|
||||
|
||||
def _technical_package():
|
||||
|
||||
Reference in New Issue
Block a user