688 lines
25 KiB
Python
688 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Validate the blocked, host-only Phase-0.9C feasibility closure."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
from typing import Any
|
|
|
|
|
|
EXPECTED_CLASSIFICATION = "BLOCKED_MULTIPLE_FOUNDATIONAL_CONTRACTS"
|
|
EXPECTED_BASELINE = "3ddc213ea67bb286256ae42e52c65e00488608ca"
|
|
EXPECTED_BRANCH = "codex/chimera-gfx-phase09c-execution-feasibility"
|
|
BLOCKED_HASH = (
|
|
"4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63"
|
|
)
|
|
|
|
AUTHORIZATION_FIELDS = (
|
|
"authorized",
|
|
"transfer_authorized",
|
|
"execution_authorized",
|
|
"installation_authorized",
|
|
"lifecycle_authorized",
|
|
"autoload_authorized",
|
|
"backup_creation_authorized",
|
|
"observer_build_authorized",
|
|
"automatic_retry",
|
|
)
|
|
|
|
SOURCE_COMMITS = {
|
|
"hardened_elfldr": (
|
|
"../chimera-elfldr",
|
|
"197623058f509eddde18868dafcb92fdcac66464",
|
|
),
|
|
"controlled_payload_manager": (
|
|
"../chimera-ps5-payload-manager",
|
|
"e23d94ff91233aa770e2342800c1467875bdef44",
|
|
),
|
|
"elfldr_public_base": (
|
|
"work/upstream/elfldr-v0.23",
|
|
"699e8bcff03e91e8d6ca6eba281af25c5a58d8c2",
|
|
),
|
|
"payload_manager_public_base": (
|
|
"work/upstream/pldmgr-v0.3.1",
|
|
"cfbc70f30f419b09bf2b52283f7409e2d3117ee1",
|
|
),
|
|
"ps5_payload_sdk_v0_41": (
|
|
"work/upstream/sdk",
|
|
"d2e2e585740362976a39fdd5ccf390f199a7bc37",
|
|
),
|
|
}
|
|
|
|
SOURCE_HASHES = {
|
|
"../chimera-elfldr/elfldr.c": (
|
|
"9949f8e4037984d10f1f5aa498e4665593d1fac8a33614d7f2141349839bb803"
|
|
),
|
|
"../chimera-elfldr/socksrv.c": (
|
|
"d642ced3e9b4a296dd15e355050ebe956f53a6dfdaa6ac10109cd067a3bba3d7"
|
|
),
|
|
"../chimera-elfldr/controlled_runtime.c": (
|
|
"10145f3bbb3b54e3d715b1667e45c6f12d3c7f52a04342ab104ec8dd49e384c9"
|
|
),
|
|
"../chimera-elfldr/ps5_controlled.c": (
|
|
"68717ef1cc31c483743c5af325c59e73a90f66e93b880b989e349a4ddc748772"
|
|
),
|
|
"../chimera-ps5-payload-manager/src/verified_launcher.c": (
|
|
"066100ca4917c7acc560e2e85666ca136cd7ccfd9094417377048f41106dd56e"
|
|
),
|
|
"../chimera-ps5-payload-manager/src/ps5_launcher.c": (
|
|
"29c1a5fd01784a59e88b3698940f120cb03020071bc2b7d74a1da1a51524ef59"
|
|
),
|
|
"work/upstream/sdk/crt/crt.c": (
|
|
"3875f4739ec40b33f1f4967a1acbb585a527d2c281a360153e1f69c8b945932a"
|
|
),
|
|
"work/upstream/sdk/crt/patch.c": (
|
|
"4f76a677bba54f4641e1cf2755768c29afb7464a35b2f21e85db2aa2785eceac"
|
|
),
|
|
"work/upstream/sdk/crt/Makefile": (
|
|
"6a62f777f32ab05cbe7bff81c00cfd6639ba8bf440aba611a40ff0b62732568b"
|
|
),
|
|
"work/upstream/sdk/host/bin/prospero-clang": (
|
|
"0cf49ae43d6110a7606c0ee4d702fc4b5d5e1c3ae9a722945a48b80294e295ba"
|
|
),
|
|
"work/upstream/sdk/host/elf_x86_64.x": (
|
|
"169b80d01da601ef96bbc584986608dec1d9c01397eae81eccd4e1a66b0a6c6a"
|
|
),
|
|
"work/upstream/sdk/crt/kernel.c": (
|
|
"ac1c375aae8d3cb1be5fb8bad2f4e6492b6ec1f1450977b534202f46b5b70321"
|
|
),
|
|
"work/upstream/sdk/sce_stubs/libkernel_web.c": (
|
|
"dca70757a0680ede52502fe7db10060fe30506f7ed0adfc43ce00700145ab4ff"
|
|
),
|
|
"samples/lifecycle_probe/main.c": (
|
|
"1ae7df1fe921ccab2a252f77975d3d441ef7725e34535b024580c0d4a242d766"
|
|
),
|
|
}
|
|
|
|
IMMUTABLE_HASHES = {
|
|
"docs/runtime/phase-0.8-read-only-preflight.md": (
|
|
"3fbe086175a6048176075f447ec1482074928e3b5282db97ea2169395fe1d508"
|
|
),
|
|
"manifests/runtime/phase-0.8-read-only-preflight.json": (
|
|
"47d7f452f8799979fe99b3e6d56859f03544112725bf7e5b349eba5ed81b3322"
|
|
),
|
|
"tests/test_phase08_preflight.py": (
|
|
"8a4ad7c70de28ffe3148fd3fd1f68c36a872c53c691c9068e1ff163970863c48"
|
|
),
|
|
"manifests/runtime/phase-0.9-anti-brick-design.json": (
|
|
"39fd7c70cae998d9d74e7caf1ff3c19f9f76f7de2e5a50bc09baede4ba53e9e9"
|
|
),
|
|
"manifests/runtime/phase-0.9b-observer.json": (
|
|
"104c4a667ad17f9827fc7276852c6faeef96effb8e3561a5ebc19a62c7d51634"
|
|
),
|
|
"manifests/runtime/phase-0.9b-observation-plan.schema.json": (
|
|
"efcea3b0001ef5b2da65c372ceb93ee2fec09c9331b2e4cbb6008212504c0918"
|
|
),
|
|
}
|
|
|
|
CHECKSUM_FILES = (
|
|
"docs/runtime/phase-0.9c-capability-closure.md",
|
|
"docs/runtime/phase-0.9c-output-channel-feasibility.md",
|
|
"docs/runtime/phase-0.9c-side-effect-model.md",
|
|
"docs/runtime/phase-0.9c-startup-exit-feasibility.md",
|
|
"docs/runtime/phase-0.9c-static-audit.md",
|
|
"manifests/runtime/phase-0.9c-feasibility.json",
|
|
"manifests/runtime/phase-0.9c-feasibility.schema.json",
|
|
"tests/phase09c_feasibility_model.py",
|
|
"tests/test_phase09c_feasibility.py",
|
|
"tests/test_phase09c_protocol.py",
|
|
"tools/validate_phase09c_feasibility.py",
|
|
)
|
|
|
|
CAPABILITIES = {
|
|
"runtime_self_identity",
|
|
"firmware_source_1",
|
|
"firmware_source_2",
|
|
"mount_query",
|
|
"metadata",
|
|
"object_id",
|
|
"size",
|
|
"sha256",
|
|
"processes",
|
|
"services",
|
|
"listeners",
|
|
"autoload",
|
|
"rollback_objects",
|
|
"monotonic_time",
|
|
"startup",
|
|
"output",
|
|
"normal_exit",
|
|
"error_exit",
|
|
"timeout",
|
|
"cleanup",
|
|
"recovery_independence",
|
|
}
|
|
|
|
OUTPUT_ARCHITECTURES = (
|
|
(
|
|
"D1_CALLER_OWNED_BOUNDED_BUFFER",
|
|
"CONCEPT_FEASIBLE_REQUIRES_LOADER_CHANGE_AND_EXIT_PROOF",
|
|
),
|
|
("D2_EXISTING_REQUEST_RESPONSE", "REJECTED_SEND_ONLY_NO_RESULT_RECEIVE"),
|
|
(
|
|
"D3_LOADER_OWNED_STATUS_RECORD",
|
|
"UNPROVEN_REQUIRES_LOADER_STATE_AND_PROPAGATION_CHANGE",
|
|
),
|
|
(
|
|
"D4_PROCESS_EXIT_STATUS",
|
|
"REJECTED_WAIT_STATUS_DISCARDED_AND_AMBIGUOUS",
|
|
),
|
|
)
|
|
|
|
FORBIDDEN_PHASE09C_SUFFIXES = {
|
|
".c",
|
|
".cc",
|
|
".cpp",
|
|
".cxx",
|
|
".s",
|
|
".asm",
|
|
".o",
|
|
".obj",
|
|
".elf",
|
|
".self",
|
|
".sprx",
|
|
".map",
|
|
".pkg",
|
|
".zip",
|
|
".tar",
|
|
".gz",
|
|
}
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
value = json.load(handle)
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"{path} must contain a JSON object")
|
|
return value
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def git_output(root: Path, *args: str) -> str:
|
|
return subprocess.check_output(
|
|
["git", *args], cwd=root, text=True, encoding="utf-8"
|
|
).strip()
|
|
|
|
|
|
def _json_type_matches(expected: str, value: Any) -> bool:
|
|
if expected == "object":
|
|
return isinstance(value, dict)
|
|
if expected == "array":
|
|
return isinstance(value, list)
|
|
if expected == "string":
|
|
return isinstance(value, str)
|
|
if expected == "integer":
|
|
return isinstance(value, int) and not isinstance(value, bool)
|
|
if expected == "boolean":
|
|
return isinstance(value, bool)
|
|
if expected == "null":
|
|
return value is None
|
|
return True
|
|
|
|
|
|
def validate_schema_instance(
|
|
schema: dict[str, Any], value: Any, path: str = "$"
|
|
) -> list[str]:
|
|
"""Validate the JSON-Schema features used by the Phase-0.9C schema."""
|
|
|
|
errors: list[str] = []
|
|
if "const" in schema and value != schema["const"]:
|
|
errors.append(f"{path} differs from schema const")
|
|
return errors
|
|
expected_type = schema.get("type")
|
|
if isinstance(expected_type, str) and not _json_type_matches(expected_type, value):
|
|
errors.append(f"{path} is not {expected_type}")
|
|
return errors
|
|
|
|
if isinstance(value, dict):
|
|
required = schema.get("required", [])
|
|
for key in required:
|
|
if key not in value:
|
|
errors.append(f"{path}.{key} is required")
|
|
properties = schema.get("properties", {})
|
|
if schema.get("additionalProperties") is False:
|
|
for key in value:
|
|
if key not in properties:
|
|
errors.append(f"{path}.{key} is not allowed")
|
|
for key, child_schema in properties.items():
|
|
if key in value:
|
|
errors.extend(
|
|
validate_schema_instance(child_schema, value[key], f"{path}.{key}")
|
|
)
|
|
|
|
if isinstance(value, list):
|
|
minimum = schema.get("minItems")
|
|
maximum = schema.get("maxItems")
|
|
if isinstance(minimum, int) and len(value) < minimum:
|
|
errors.append(f"{path} has fewer than {minimum} items")
|
|
if isinstance(maximum, int) and len(value) > maximum:
|
|
errors.append(f"{path} has more than {maximum} items")
|
|
if schema.get("uniqueItems") is True:
|
|
normalized = [json.dumps(item, sort_keys=True) for item in value]
|
|
if len(normalized) != len(set(normalized)):
|
|
errors.append(f"{path} contains duplicate items")
|
|
item_schema = schema.get("items")
|
|
if isinstance(item_schema, dict):
|
|
for index, item in enumerate(value):
|
|
errors.extend(
|
|
validate_schema_instance(item_schema, item, f"{path}[{index}]")
|
|
)
|
|
return errors
|
|
|
|
|
|
def validate_authorizations(value: dict[str, Any]) -> list[str]:
|
|
errors: list[str] = []
|
|
for field in AUTHORIZATION_FIELDS:
|
|
if value.get(field) is not False:
|
|
errors.append(f"authorization.{field} must be false")
|
|
return errors
|
|
|
|
|
|
def validate_manifest(manifest: dict[str, Any]) -> list[str]:
|
|
errors: list[str] = []
|
|
if manifest.get("schema_version") != 1 or manifest.get("phase") != "0.9C":
|
|
errors.append("Phase-0.9C manifest identity changed")
|
|
if manifest.get("status") != "BLOCKED":
|
|
errors.append("Phase-0.9C status must remain BLOCKED")
|
|
if manifest.get("classification") != EXPECTED_CLASSIFICATION:
|
|
errors.append("Phase-0.9C classification changed")
|
|
if manifest.get("baseline_commit") != EXPECTED_BASELINE:
|
|
errors.append("Phase-0.9C baseline changed")
|
|
if manifest.get("branch") != EXPECTED_BRANCH:
|
|
errors.append("Phase-0.9C branch changed")
|
|
errors.extend(validate_authorizations(manifest.get("authorization", {})))
|
|
|
|
canonical = manifest.get("canonical_state_preserved", {})
|
|
expected_canonical = {
|
|
"historical_phase08_status": "READ_ONLY_PREFLIGHT_BLOCKED",
|
|
"phase09a_status": "DESIGN_ONLY",
|
|
"phase09b_status": "BLOCKED",
|
|
"firmware_runtime_behavior": "UNPROVEN",
|
|
"stock_hashes": "reference_only",
|
|
"payload_manager_backup": "HARD_BLOCKER",
|
|
"device_contact_performed": False,
|
|
"device_transfer_performed": False,
|
|
"device_execution_performed": False,
|
|
"target_build_performed": False,
|
|
}
|
|
if canonical != expected_canonical:
|
|
errors.append("canonical Phase-0.8/0.9 state changed")
|
|
|
|
deny = manifest.get("permanent_denylist_binding", {})
|
|
if deny != {
|
|
"sha256": BLOCKED_HASH,
|
|
"status": "BLOCKED",
|
|
"permanent": True,
|
|
"execution_eligible": False,
|
|
}:
|
|
errors.append("permanent artifact denial changed")
|
|
|
|
source_commits = manifest.get("source_commits", {})
|
|
for key, (_, expected_commit) in SOURCE_COMMITS.items():
|
|
if source_commits.get(key) != expected_commit:
|
|
errors.append(f"source commit changed: {key}")
|
|
if source_commits.get("lifecycle_probe_source") != (
|
|
"fe08300339a13f899fb78ea404ada381a5cba87c"
|
|
):
|
|
errors.append("lifecycle source commit changed")
|
|
if manifest.get("source_tree_status") != {
|
|
key: "clean" for key in SOURCE_COMMITS
|
|
}:
|
|
errors.append("source tree status record changed")
|
|
|
|
evidence = {
|
|
item.get("path"): item.get("sha256")
|
|
for item in manifest.get("source_evidence", [])
|
|
if isinstance(item, dict)
|
|
}
|
|
if evidence != SOURCE_HASHES:
|
|
errors.append("source evidence path/hash inventory changed")
|
|
|
|
immutable = manifest.get("immutable_evidence", {})
|
|
if immutable.get("phase08", {}).get("files") != {
|
|
key: IMMUTABLE_HASHES[key]
|
|
for key in (
|
|
"docs/runtime/phase-0.8-read-only-preflight.md",
|
|
"manifests/runtime/phase-0.8-read-only-preflight.json",
|
|
"tests/test_phase08_preflight.py",
|
|
)
|
|
}:
|
|
errors.append("Phase-0.8 immutable manifest bindings changed")
|
|
if immutable.get("phase09a_manifest_sha256") != IMMUTABLE_HASHES[
|
|
"manifests/runtime/phase-0.9-anti-brick-design.json"
|
|
]:
|
|
errors.append("Phase-0.9A manifest binding changed")
|
|
if immutable.get("phase09b_manifest_sha256") != IMMUTABLE_HASHES[
|
|
"manifests/runtime/phase-0.9b-observer.json"
|
|
]:
|
|
errors.append("Phase-0.9B manifest binding changed")
|
|
if immutable.get("phase09b_schema_sha256") != IMMUTABLE_HASHES[
|
|
"manifests/runtime/phase-0.9b-observation-plan.schema.json"
|
|
]:
|
|
errors.append("Phase-0.9B schema binding changed")
|
|
|
|
startup = manifest.get("startup_exit", {})
|
|
false_startup_fields = (
|
|
"normal_sdk_kernelwrite_free",
|
|
"freestanding_dependency_closure_proven",
|
|
"stack_alignment_proven",
|
|
"complete_relocation_and_bss_tls_contract_proven",
|
|
"callable_read_and_time_abi_proven",
|
|
"safe_return_proven",
|
|
"safe_process_exit_proven",
|
|
"error_exit_proven",
|
|
"timeout_safe_exit_proven",
|
|
"complete_cleanup_proven",
|
|
)
|
|
for field in false_startup_fields:
|
|
if startup.get(field) is not False:
|
|
errors.append(f"startup_exit.{field} must be false")
|
|
if startup.get("normal_sdk_status") != "PROVEN_SIDE_EFFECTING":
|
|
errors.append("normal SDK startup side effects were hidden")
|
|
if not startup.get("blockers"):
|
|
errors.append("startup/exit blockers are absent")
|
|
|
|
architectures = manifest.get("output_architectures", [])
|
|
observed_architectures = tuple(
|
|
(item.get("id"), item.get("status"))
|
|
for item in architectures
|
|
if isinstance(item, dict)
|
|
)
|
|
if observed_architectures != OUTPUT_ARCHITECTURES:
|
|
errors.append("output architecture decisions changed or reordered")
|
|
if any(item.get("current_implementation") is not False for item in architectures):
|
|
errors.append("an output architecture claims current implementation")
|
|
|
|
protocol = manifest.get("host_protocol", {})
|
|
expected_protocol = {
|
|
"model": "tests/phase09c_feasibility_model.py",
|
|
"host_only": True,
|
|
"target_implemented": False,
|
|
"magic": "CHG09C01",
|
|
"version": 1,
|
|
"header_size": 256,
|
|
"maximum_output_size": 4096,
|
|
"maximum_body_size": 3840,
|
|
"integer_encoding": "unsigned_big_endian",
|
|
"execution_nonce_bytes": 16,
|
|
"request_id_bytes": 16,
|
|
"firmware_field_bytes": 8,
|
|
"artifact_hash_algorithm": "sha256",
|
|
"body_checksum_algorithm": "sha256",
|
|
"result_checksum_algorithm": "sha256",
|
|
"completion_marker": "COMPLETE",
|
|
"pointers_present": False,
|
|
"dynamic_growth": False,
|
|
}
|
|
for field, expected in expected_protocol.items():
|
|
if protocol.get(field) != expected:
|
|
errors.append(f"host protocol field changed: {field}")
|
|
if len(protocol.get("required_fields", [])) < 16:
|
|
errors.append("host protocol required fields are incomplete")
|
|
if len(protocol.get("fail_closed_conditions", [])) < 10:
|
|
errors.append("host protocol fail-closed cases are incomplete")
|
|
|
|
firmware = manifest.get("firmware", {})
|
|
source_two = firmware.get("source_two", {})
|
|
if firmware.get("expected") != "9.60":
|
|
errors.append("exact firmware gate changed")
|
|
if source_two != {
|
|
"identity": None,
|
|
"status": "ABSENT",
|
|
"export_name_candidate_accepted": False,
|
|
"nonce_bound_runtime_result_present": False,
|
|
}:
|
|
errors.append("firmware source two was fabricated or promoted")
|
|
if firmware.get("agreement_proven") is not False:
|
|
errors.append("firmware agreement was claimed")
|
|
if firmware.get("gate") != "BLOCKED_FIRMWARE_SOURCE_INCOMPLETE":
|
|
errors.append("firmware gate was promoted")
|
|
|
|
side_effects = manifest.get("side_effect_model", {})
|
|
for field in (
|
|
"no_persistent_content_write_is_side_effect_free",
|
|
"read_only_flag_is_side_effect_free",
|
|
"all_planned_observations_proven_side_effect_free",
|
|
):
|
|
if side_effects.get(field) is not False:
|
|
errors.append(f"side-effect claim must remain false: {field}")
|
|
if side_effects.get("gate") != "BLOCKED_OBSERVATION_SIDE_EFFECTS_UNBOUNDED":
|
|
errors.append("side-effect gate changed")
|
|
required_dimensions = {
|
|
"content",
|
|
"metadata",
|
|
"atime",
|
|
"audit",
|
|
"cache",
|
|
"counters",
|
|
"service_state",
|
|
"security_monitoring",
|
|
"open_bookkeeping",
|
|
"process_accounting",
|
|
"object_lifetime",
|
|
"races",
|
|
}
|
|
if set(side_effects.get("dimensions", [])) != required_dimensions:
|
|
errors.append("side-effect dimensions are incomplete")
|
|
|
|
capabilities = manifest.get("capability_closure", [])
|
|
if {item.get("id") for item in capabilities if isinstance(item, dict)} != CAPABILITIES:
|
|
errors.append("capability closure inventory changed")
|
|
for item in capabilities:
|
|
if not isinstance(item, dict):
|
|
errors.append("capability entry is not an object")
|
|
continue
|
|
if item.get("implementation_allowed") is not False:
|
|
errors.append(f"capability implementation enabled: {item.get('id')}")
|
|
if item.get("execution_allowed") is not False:
|
|
errors.append(f"capability execution enabled: {item.get('id')}")
|
|
if item.get("target_evidence") in (None, "", "PROVEN"):
|
|
errors.append(f"capability target evidence invalid: {item.get('id')}")
|
|
if not item.get("blocker"):
|
|
errors.append(f"capability blocker absent: {item.get('id')}")
|
|
|
|
implementation = manifest.get("implementation", {})
|
|
if not implementation or any(value is not False for value in implementation.values()):
|
|
errors.append("target/runtime implementation state must be entirely false")
|
|
if manifest.get("artifact") != {
|
|
"present": False,
|
|
"path": None,
|
|
"sha256": None,
|
|
"size": None,
|
|
"execution_eligible": False,
|
|
"execution_authorized": False,
|
|
}:
|
|
errors.append("Phase-0.9C artifact must not exist")
|
|
static = manifest.get("static_audit", {})
|
|
if static.get("status") != "NOT_APPLICABLE_NO_TARGET_SOURCE_OR_ARTIFACT":
|
|
errors.append("static target audit was falsely promoted")
|
|
if static.get("host_model_only") is not True:
|
|
errors.append("static audit is not explicitly host-only")
|
|
|
|
decision = manifest.get("final_decision", {})
|
|
if decision.get("positive_classification_allowed") is not False:
|
|
errors.append("positive classification was enabled")
|
|
if decision.get("classification") != EXPECTED_CLASSIFICATION:
|
|
errors.append("final decision differs from top-level classification")
|
|
expected_blockers = {
|
|
"BLOCKED_STARTUP_ABI_UNPROVEN",
|
|
"BLOCKED_EXIT_CLEANUP_UNPROVEN",
|
|
"BLOCKED_NO_BOUNDED_OUTPUT_CHANNEL",
|
|
"BLOCKED_FIRMWARE_SOURCE_INCOMPLETE",
|
|
"BLOCKED_OBSERVATION_SIDE_EFFECTS_UNBOUNDED",
|
|
}
|
|
if set(decision.get("foundational_blockers", [])) != expected_blockers:
|
|
errors.append("foundational blocker set changed")
|
|
if decision.get("next_phase_automatic") is not False:
|
|
errors.append("automatic next phase was enabled")
|
|
return errors
|
|
|
|
|
|
def validate_source_trees(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
for key, (relative, expected_commit) in SOURCE_COMMITS.items():
|
|
source_root = (root / relative).resolve()
|
|
if not source_root.is_dir():
|
|
errors.append(f"source tree missing: {key}")
|
|
continue
|
|
try:
|
|
commit = git_output(source_root, "rev-parse", "HEAD")
|
|
status = git_output(source_root, "status", "--short")
|
|
except subprocess.CalledProcessError:
|
|
errors.append(f"source tree is not readable Git: {key}")
|
|
continue
|
|
if commit != expected_commit:
|
|
errors.append(f"source tree commit mismatch: {key}")
|
|
if status:
|
|
errors.append(f"source tree is dirty: {key}")
|
|
|
|
for relative, expected_hash in SOURCE_HASHES.items():
|
|
path = (root / relative).resolve()
|
|
if not path.is_file():
|
|
errors.append(f"source evidence missing: {relative}")
|
|
elif sha256_file(path) != expected_hash:
|
|
errors.append(f"source evidence hash mismatch: {relative}")
|
|
return errors
|
|
|
|
|
|
def validate_immutable_evidence(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
for relative, expected_hash in IMMUTABLE_HASHES.items():
|
|
path = root / relative
|
|
if not path.is_file():
|
|
errors.append(f"immutable evidence missing: {relative}")
|
|
elif sha256_file(path) != expected_hash:
|
|
errors.append(f"immutable evidence changed: {relative}")
|
|
return errors
|
|
|
|
|
|
def forbidden_repository_path(relative: str) -> bool:
|
|
normalized = relative.replace("\\", "/").lower()
|
|
path = Path(normalized)
|
|
phase_marker = "phase09c" in normalized or "phase-0.9c" in normalized
|
|
if phase_marker and path.suffix in FORBIDDEN_PHASE09C_SUFFIXES:
|
|
return True
|
|
if normalized.startswith(
|
|
(
|
|
"samples/phase09c",
|
|
"samples/phase-0.9c",
|
|
"src/backends/ps5/phase09c",
|
|
"src/backends/ps5/observer",
|
|
)
|
|
):
|
|
return True
|
|
if normalized.startswith("packaging/phase09c/"):
|
|
return normalized != "packaging/phase09c/sha256sums.txt"
|
|
return False
|
|
|
|
|
|
def validate_repository_boundary(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
listed = git_output(
|
|
root, "ls-files", "--cached", "--others", "--exclude-standard"
|
|
).splitlines()
|
|
for relative in listed:
|
|
if forbidden_repository_path(relative):
|
|
errors.append(f"forbidden Phase-0.9C target/package path: {relative}")
|
|
|
|
cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8")
|
|
if re.search(
|
|
r"add_(?:executable|library)\s*\([^)]*phase[-_]?0?9c",
|
|
cmake,
|
|
flags=re.IGNORECASE | re.DOTALL,
|
|
):
|
|
errors.append("CMake declares a Phase-0.9C target")
|
|
forbidden_directories = (
|
|
root / "samples/phase09c_observer",
|
|
root / "samples/phase-0.9c-observer",
|
|
root / "packaging/phase09c/lifecycle",
|
|
root / "packaging/phase09c/install",
|
|
root / "packaging/phase09c/autoload",
|
|
)
|
|
for path in forbidden_directories:
|
|
if path.exists():
|
|
errors.append(f"forbidden Phase-0.9C path exists: {path.relative_to(root)}")
|
|
return errors
|
|
|
|
|
|
def validate_checksums(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
checksum_path = root / "packaging/phase09c/SHA256SUMS.txt"
|
|
if not checksum_path.is_file():
|
|
return ["Phase-0.9C checksum file is missing"]
|
|
observed: dict[str, str] = {}
|
|
for line in checksum_path.read_text(encoding="utf-8").splitlines():
|
|
parts = line.split(" ", 1)
|
|
if len(parts) != 2 or not re.fullmatch(r"[0-9a-f]{64}", parts[0]):
|
|
errors.append("malformed Phase-0.9C checksum line")
|
|
continue
|
|
observed[parts[1]] = parts[0]
|
|
if tuple(observed) != CHECKSUM_FILES:
|
|
errors.append("Phase-0.9C checksum inventory or order changed")
|
|
for relative in CHECKSUM_FILES:
|
|
path = root / relative
|
|
if not path.is_file():
|
|
errors.append(f"Phase-0.9C checksummed file missing: {relative}")
|
|
elif observed.get(relative) != sha256_file(path):
|
|
errors.append(f"Phase-0.9C checksum mismatch: {relative}")
|
|
return errors
|
|
|
|
|
|
def collect_errors(root: Path) -> list[str]:
|
|
manifest_path = root / "manifests/runtime/phase-0.9c-feasibility.json"
|
|
schema_path = root / "manifests/runtime/phase-0.9c-feasibility.schema.json"
|
|
manifest = load_json(manifest_path)
|
|
schema = load_json(schema_path)
|
|
errors = validate_manifest(manifest)
|
|
errors.extend(validate_schema_instance(schema, manifest))
|
|
errors.extend(validate_source_trees(root))
|
|
errors.extend(validate_immutable_evidence(root))
|
|
errors.extend(validate_repository_boundary(root))
|
|
errors.extend(validate_checksums(root))
|
|
|
|
denylist = load_json(root / "manifests/artifact-denylist.json")
|
|
entries = denylist.get("entries", [])
|
|
if (
|
|
len(entries) != 1
|
|
or entries[0].get("sha256") != BLOCKED_HASH
|
|
or entries[0].get("status") != "BLOCKED"
|
|
or entries[0].get("permanent") is not True
|
|
or entries[0].get("execution_eligible") is not False
|
|
):
|
|
errors.append("permanent denylist no longer blocks the legacy artifact")
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
errors = collect_errors(root)
|
|
if errors:
|
|
for error in errors:
|
|
print(f"Phase-0.9C validation failed: {error}")
|
|
return 1
|
|
print(
|
|
"Phase-0.9C feasibility manifest, schema, sources, immutable evidence, "
|
|
"denylist, checksums, and no-target boundary: PASS"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|