fix: bind rollback rehearsal v2 source closure

This commit is contained in:
2026-09-27 03:26:45 +08:00
parent 56021cfeed
commit 007193e35b
2 changed files with 284 additions and 28 deletions
+93 -1
View File
@@ -64,8 +64,35 @@ REHEARSAL_TOOL_INPUTS = (
"yuqei-infra/deploy/release-package.py",
"yuqei-infra/deploy/preflight-production.py",
"yuqei-infra/deploy/verify-production-compose.py",
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
"yuqei-infra/scripts/build-installable-release.py",
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
"yuqei-infra/scripts/build-third-party-materials.py",
"yuqei-infra/scripts/linux_release_common.py",
"yuqei-infra/release-trust/third-party-materials-baseline.json",
)
PACKAGE_SCHEMA_V1 = "yuqei.release-package.v1"
PACKAGE_SCHEMA_V2 = "yuqei.release-package.v2"
V1_BOOTSTRAP_ARTIFACTS = frozenset(
{
"release_manifest",
"checksum_manifest",
"image_identity",
"installable_manifest",
}
)
V2_BOOTSTRAP_ARTIFACTS = frozenset(
{
*V1_BOOTSTRAP_ARTIFACTS,
"infrastructure_source_set",
"infrastructure_image_bundle",
"third_party_materials_input",
"third_party_materials_manifest",
"third_party_materials_verification_receipt",
}
)
OPTIONAL_BOOTSTRAP_ARTIFACTS = frozenset(
{"backup_proof", "compatibility_proof"}
)
EXPECTED_APPLICATION_SERVICES = (
"contract-api-migrate",
@@ -119,6 +146,15 @@ def _load_release_package_module(
"yuqei-infra/deploy/verify-production-compose.py": (
"yuqei_release_package_compose_gate"
),
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py": (
"yuqei_release_package_snapshot_restore_planner"
),
"yuqei-infra/scripts/build-infrastructure-image-bundle.py": (
"yuqei_release_package_infrastructure_bundle"
),
"yuqei-infra/scripts/build-third-party-materials.py": (
"yuqei_release_package_third_party_materials"
),
"yuqei-infra/deploy/release-package.py": name,
}
previous_modules = {
@@ -135,6 +171,9 @@ def _load_release_package_module(
"yuqei-infra/scripts/build-installable-release.py",
"yuqei-infra/deploy/preflight-production.py",
"yuqei-infra/deploy/verify-production-compose.py",
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
"yuqei-infra/scripts/build-third-party-materials.py",
"yuqei-infra/deploy/release-package.py",
):
loaded[relative] = _exec_verified_module(
@@ -152,6 +191,38 @@ def _load_release_package_module(
release_package._COMPOSE_GATE_MODULE = loaded[
"yuqei-infra/deploy/verify-production-compose.py"
]
release_package._SNAPSHOT_RESTORE_PLANNER_MODULE = loaded[
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py"
]
release_package._INFRASTRUCTURE_BUNDLE_MODULE = loaded[
"yuqei-infra/scripts/build-infrastructure-image-bundle.py"
]
release_package._THIRD_PARTY_MATERIALS_MODULE = loaded[
"yuqei-infra/scripts/build-third-party-materials.py"
]
# The snapshot planner normally discovers release-package.py from
# its working tree. Keep the circular validation path on the same
# already verified module bytes instead of permitting an ambient
# file to replace the candidate after bootstrap.
snapshot_planner = release_package._SNAPSHOT_RESTORE_PLANNER_MODULE
snapshot_planner._load_release_package_validator = (
lambda _repo_root: release_package.validate_release_package
)
baseline_relative = (
"yuqei-infra/release-trust/third-party-materials-baseline.json"
)
baseline_raw, baseline_sha = _read_secure_bytes(
REPO_ROOT / baseline_relative,
label="verified third-party materials trust baseline",
max_bytes=MAX_JSON_BYTES,
)
if (
baseline_raw != verified_sources[baseline_relative]
or hashlib.sha256(baseline_raw).hexdigest() != baseline_sha
):
raise RehearsalProofError(
"Third-party materials trust baseline changed after bootstrap."
)
return release_package
except Exception as exc:
raise RehearsalProofError(
@@ -1522,11 +1593,32 @@ def _bootstrap_verified_tool_sources(
current_package_sha256,
label="current release-package.json bootstrap",
)
if package.get("schema_version") != "yuqei.release-package.v1":
package_schema = package.get("schema_version")
if package_schema not in {PACKAGE_SCHEMA_V1, PACKAGE_SCHEMA_V2}:
raise RehearsalProofError("Current release package bootstrap schema is unsupported.")
artifacts = package.get("artifacts")
if not isinstance(artifacts, dict):
raise RehearsalProofError("Current release package bootstrap has no artifact inventory.")
artifact_names = set(artifacts)
required_artifacts = (
V2_BOOTSTRAP_ARTIFACTS
if package_schema == PACKAGE_SCHEMA_V2
else V1_BOOTSTRAP_ARTIFACTS
)
if (
not required_artifacts.issubset(artifact_names)
or not artifact_names.issubset(
required_artifacts | OPTIONAL_BOOTSTRAP_ARTIFACTS
)
):
if package_schema == PACKAGE_SCHEMA_V2:
raise RehearsalProofError(
"Current release package v2 bootstrap artifact closure is incomplete."
)
raise RehearsalProofError(
"Legacy release package v1 bootstrap must remain read-only and must not "
"contain v2 or unknown artifacts."
)
release_record = _require_exact_fields(
artifacts.get("release_manifest"),
{"path", "sha256"},
@@ -42,6 +42,62 @@ def _write_json(path: Path, payload: dict[str, Any]) -> str:
return hashlib.sha256(raw).hexdigest()
def _bootstrap_package_fixture(
tmp_path: Path,
*,
schema: str,
omitted_tool_input: str | None = None,
omitted_artifact: str | None = None,
extra_artifact: str | None = None,
repo_root: Path = REPO_ROOT,
) -> tuple[Path, str, dict[str, bytes]]:
source_items = []
expected_raw: dict[str, bytes] = {}
for relative in planner.REHEARSAL_TOOL_INPUTS:
source = repo_root / relative
raw = source.read_bytes()
expected_raw[relative] = raw
if relative != omitted_tool_input:
source_items.append(
{
"path": relative,
"sha256": hashlib.sha256(raw).hexdigest(),
}
)
source_manifest = {
"schema_version": "yuqei.release-source-manifest.v1",
"files": {"items": source_items},
}
release_dir = tmp_path / "release"
source_path = release_dir / "release-source-manifest.json"
source_sha = _write_json(source_path, source_manifest)
required = (
planner.V2_BOOTSTRAP_ARTIFACTS
if schema == planner.PACKAGE_SCHEMA_V2
else planner.V1_BOOTSTRAP_ARTIFACTS
)
artifacts = {
name: {"path": f"artifacts/{name}.json", "sha256": "0" * 64}
for name in required
if name != omitted_artifact
}
artifacts["release_manifest"] = {
"path": "release-source-manifest.json",
"sha256": source_sha,
}
if extra_artifact is not None:
artifacts[extra_artifact] = {
"path": f"artifacts/{extra_artifact}.json",
"sha256": "1" * 64,
}
package_path = release_dir / "release-package.json"
package_sha = _write_json(
package_path,
{"schema_version": schema, "artifacts": artifacts},
)
return package_path, package_sha, expected_raw
def _with_hash(payload: dict[str, Any], field: str) -> dict[str, Any]:
return {**payload, field: planner.canonical_sha256(payload)}
@@ -799,33 +855,10 @@ def test_rollback_tool_sources_are_bound_to_current_package(tmp_path: Path) -> N
def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
tmp_path: Path,
) -> None:
source_items = []
expected_raw = {}
for relative in planner.REHEARSAL_TOOL_INPUTS:
source = REPO_ROOT / relative
raw = source.read_bytes()
expected_raw[relative] = raw
source_items.append({
"path": relative,
"sha256": hashlib.sha256(raw).hexdigest(),
})
source_manifest = {
"schema_version": "yuqei.release-source-manifest.v1",
"files": {"items": source_items},
}
source_path = tmp_path / "release" / "release-source-manifest.json"
source_sha = _write_json(source_path, source_manifest)
package = {
"schema_version": "yuqei.release-package.v1",
"artifacts": {
"release_manifest": {
"path": "release-source-manifest.json",
"sha256": source_sha,
}
},
}
package_path = tmp_path / "release" / "release-package.json"
package_sha = _write_json(package_path, package)
package_path, package_sha, expected_raw = _bootstrap_package_fixture(
tmp_path,
schema=planner.PACKAGE_SCHEMA_V2,
)
verified_sources, verified_hashes = planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
@@ -841,6 +874,15 @@ def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
assert loaded._INSTALLABLE_VALIDATOR_MODULE is not None
assert loaded._PREFLIGHT_MODULE is not None
assert loaded._COMPOSE_GATE_MODULE is not None
assert loaded._INFRASTRUCTURE_BUNDLE_MODULE is not None
assert loaded._THIRD_PARTY_MATERIALS_MODULE is not None
assert loaded._SNAPSHOT_RESTORE_PLANNER_MODULE is not None
assert (
loaded._SNAPSHOT_RESTORE_PLANNER_MODULE._load_release_package_validator(
REPO_ROOT
)
is loaded.validate_release_package
)
(package_path.parent / "release-source-manifest.json").write_bytes(b"tampered\n")
with pytest.raises(planner.RehearsalProofError, match="externally supplied SHA-256"):
@@ -851,6 +893,128 @@ def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
)
@pytest.mark.parametrize(
"relative",
(
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
"yuqei-infra/scripts/build-third-party-materials.py",
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
"yuqei-infra/release-trust/third-party-materials-baseline.json",
),
)
def test_v2_bootstrap_rejects_missing_new_source_closure(
tmp_path: Path,
relative: str,
) -> None:
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
tmp_path,
schema=planner.PACKAGE_SCHEMA_V2,
omitted_tool_input=relative,
)
with pytest.raises(planner.RehearsalProofError, match="does not bind"):
planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
current_package_sha256=package_sha,
repo_root=REPO_ROOT,
)
@pytest.mark.parametrize(
"relative",
(
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
"yuqei-infra/scripts/build-third-party-materials.py",
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
"yuqei-infra/release-trust/third-party-materials-baseline.json",
),
)
def test_v2_bootstrap_rejects_new_source_drift(
tmp_path: Path,
relative: str,
) -> None:
candidate_root = tmp_path / "candidate"
for tool_input in planner.REHEARSAL_TOOL_INPUTS:
target = candidate_root / tool_input
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes((REPO_ROOT / tool_input).read_bytes())
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
tmp_path / "package",
schema=planner.PACKAGE_SCHEMA_V2,
repo_root=candidate_root,
)
(candidate_root / relative).write_bytes(b"candidate drift\n")
with pytest.raises(planner.RehearsalProofError, match="differs from package identity"):
planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
current_package_sha256=package_sha,
repo_root=candidate_root.resolve(),
)
def test_verified_v2_dependency_modules_are_injected_from_bootstrapped_bytes(
tmp_path: Path,
) -> None:
_package_path, _package_sha, verified_sources = _bootstrap_package_fixture(
tmp_path,
schema=planner.PACKAGE_SCHEMA_V2,
)
injected = dict(verified_sources)
module_paths = (
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
"yuqei-infra/scripts/build-third-party-materials.py",
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
)
for index, relative in enumerate(module_paths, start=1):
injected[relative] += f"\nVERIFIED_BYTE_SENTINEL = {index}\n".encode()
loaded = planner._load_release_package_module(injected)
assert loaded._INFRASTRUCTURE_BUNDLE_MODULE.VERIFIED_BYTE_SENTINEL == 1
assert loaded._THIRD_PARTY_MATERIALS_MODULE.VERIFIED_BYTE_SENTINEL == 2
assert loaded._SNAPSHOT_RESTORE_PLANNER_MODULE.VERIFIED_BYTE_SENTINEL == 3
def test_bootstrap_accepts_complete_legacy_v1_for_read_only_validation(
tmp_path: Path,
) -> None:
package_path, package_sha, expected_raw = _bootstrap_package_fixture(
tmp_path,
schema=planner.PACKAGE_SCHEMA_V1,
)
verified_sources, _verified_hashes = planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
current_package_sha256=package_sha,
repo_root=REPO_ROOT,
)
assert verified_sources == expected_raw
def test_bootstrap_rejects_incomplete_v2_and_v2_artifact_downgrade(
tmp_path: Path,
) -> None:
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
tmp_path / "incomplete-v2",
schema=planner.PACKAGE_SCHEMA_V2,
omitted_artifact="third_party_materials_verification_receipt",
)
with pytest.raises(planner.RehearsalProofError, match="v2 bootstrap artifact closure"):
planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
current_package_sha256=package_sha,
repo_root=REPO_ROOT,
)
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
tmp_path / "downgraded-v1",
schema=planner.PACKAGE_SCHEMA_V1,
extra_artifact="infrastructure_image_bundle",
)
with pytest.raises(planner.RehearsalProofError, match="v1 bootstrap must remain read-only"):
planner._bootstrap_verified_tool_sources(
current_package_dir=package_path.parent.resolve(),
current_package_sha256=package_sha,
repo_root=REPO_ROOT,
)
def test_strict_env_preflight_and_compose_gate_evidence_fail_closed(
tmp_path: Path, monkeypatch
) -> None: