#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Validate the blocked, offline-only Phase-0.9B observer audit.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import re import subprocess import sys from typing import Any EXPECTED_DECISION = { "OBSERVER_STARTUP_OR_EXIT_ABI_UNPROVEN", "NO_PROVEN_NON_PERSISTENT_OUTPUT_CHANNEL", } AUTHORIZATION_FIELDS = ( "authorized", "transfer_authorized", "execution_authorized", "installation_authorized", "lifecycle_authorized", "autoload_authorized", "backup_creation_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", ), } 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 extract_json_contract(path: Path, marker: str) -> dict[str, Any]: text = path.read_text(encoding="utf-8") pattern = ( rf"\s*```json\s*(.*?)\s*```\s*" rf"" ) match = re.search(pattern, text, flags=re.DOTALL) if not match: raise ValueError(f"{path} is missing {marker}") value = json.loads(match.group(1)) if not isinstance(value, dict): raise ValueError(f"{marker} must be 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 validate_authorizations(value: dict[str, Any], prefix: str) -> list[str]: errors: list[str] = [] for field in AUTHORIZATION_FIELDS: if value.get(field) is not False: errors.append(f"{prefix}.{field} must be false") return errors def validate_schema(schema: dict[str, Any], manifest: dict[str, Any]) -> list[str]: errors: list[str] = [] properties = schema.get("properties", {}) for field in AUTHORIZATION_FIELDS: if properties.get(field, {}).get("const") is not False: errors.append(f"schema {field} must be const false") if properties.get("device_address", {}).get("const", "missing") is not None: errors.append("schema device_address must be const null") if properties.get("maximum_execution_count", {}).get("const") != 0: errors.append("schema maximum_execution_count must be const zero") default = schema.get("x-chimera-default-plan") if default != manifest.get("default_observation_plan"): errors.append("schema default plan must equal manifest default plan") if not isinstance(default, dict): return errors errors.extend(validate_authorizations(default, "default_plan")) if default.get("device_address") is not None: errors.append("default plan contains a device address") if default.get("device_identity") is not None: errors.append("default plan contains a device identity") if default.get("read_paths") != []: errors.append("default plan contains read paths") if default.get("allowed_observations") != []: errors.append("default plan contains observations") if default.get("output_channel") is not None: errors.append("default plan contains an output channel") if default.get("maximum_execution_count") != 0: errors.append("default plan permits an execution") return errors def validate_manifest(root: Path, manifest: dict[str, Any]) -> list[str]: errors: list[str] = [] if manifest.get("status") != "BLOCKED": errors.append("Phase-0.9B status must be BLOCKED") if set(manifest.get("decision", [])) != EXPECTED_DECISION: errors.append("Phase-0.9B hard-gate decision changed") errors.extend( validate_authorizations(manifest.get("authorization", {}), "authorization") ) canonical = manifest.get("canonical_state_preserved", {}) expected_canonical = { "historical_phase08_status": "READ_ONLY_PREFLIGHT_BLOCKED", "phase09a_status": "DESIGN_ONLY", "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, } for field, expected in expected_canonical.items(): if canonical.get(field) != expected: errors.append(f"canonical state {field} must be {expected!r}") deny_binding = manifest.get("permanent_denylist_binding", {}) if deny_binding != { "sha256": "4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63", "status": "BLOCKED", "permanent": True, "execution_eligible": False, }: errors.append("permanent denylist binding changed") commits = manifest.get("source_commits", {}) tree_status = manifest.get("source_tree_status", {}) for key, (relative, expected_commit) in SOURCE_COMMITS.items(): if commits.get(key) != expected_commit: errors.append(f"manifest source commit mismatch: {key}") path = (root / relative).resolve() if not path.exists(): errors.append(f"source tree missing: {relative}") continue try: actual_commit = git_output(path, "rev-parse", "HEAD") dirty = git_output(path, "status", "--porcelain") except (OSError, subprocess.CalledProcessError) as exc: errors.append(f"source tree unreadable: {relative}: {exc}") continue if actual_commit != expected_commit: errors.append(f"source tree commit mismatch: {relative}") if dirty: errors.append(f"source tree is dirty: {relative}") if tree_status.get(key) != "clean": errors.append(f"manifest does not classify {key} as clean") for evidence in manifest.get("source_evidence", []): relative = evidence.get("path") expected_digest = evidence.get("sha256") if not isinstance(relative, str) or not isinstance(expected_digest, str): errors.append("source evidence entry lacks path or SHA-256") continue path = (root / relative).resolve() if not path.is_file(): errors.append(f"source evidence file missing: {relative}") continue if sha256_file(path) != expected_digest: errors.append(f"source evidence digest mismatch: {relative}") if "size" in evidence and path.stat().st_size != evidence["size"]: errors.append(f"source evidence size mismatch: {relative}") matrix = manifest.get("capability_matrix", []) expected_facts = { "firmware_source_1", "firmware_source_2", "file_metadata", "object_identity", "sha256", "mount_information", "processes_services", "listeners", "autoload_configuration", "output_channel", "monotonic_time_deadline", "process_exit", } if {entry.get("needed_fact") for entry in matrix} != expected_facts: errors.append("capability matrix is incomplete or changed") if any(entry.get("implement") is not False for entry in matrix): errors.append("a blocked capability is marked for implementation") gate = manifest.get("build_gate", {}) for field in ( "startup_and_exit_abi_proven", "non_persistent_output_channel_proven", "normal_sdk_crt_kernelwrite_free", "custom_freestanding_cleanup_proven", "observer_source_created", "observer_target_declared", "target_build_performed", "double_clean_build_performed", ): if gate.get(field) is not False: errors.append(f"build gate {field} must be false") if gate.get("reason") != "BLOCKED_BEFORE_SOURCE_AND_BUILD": errors.append("build gate reason changed") implementation = manifest.get("implementation", {}) if implementation.get("observer_logic_implemented") is not False: errors.append("observer logic must remain unimplemented") if implementation.get("observations_implemented") != []: errors.append("target observations must remain unimplemented") artifact = manifest.get("artifact", {}) if artifact.get("present") is not False: errors.append("observer artifact must be absent") for field in ("path", "sha256", "size"): if artifact.get(field) is not None: errors.append(f"artifact {field} must be null") for field in ( "imports", "undefined_symbols", "dynamic_dependencies", "network_functions", "filesystem_reads", ): if artifact.get(field) != []: errors.append(f"artifact {field} must be empty") for field in ( "installation_eligible", "lifecycle_eligible", "autoload_eligible", "execution_authorized", "execution_eligible", ): if artifact.get(field) is not False: errors.append(f"artifact {field} must be false") if ( manifest.get("static_artifact_audit", {}).get("status") != "NOT_PERFORMED_BLOCKED_BEFORE_BUILD" ): errors.append("artifact audit must be recorded as not performed") reproducibility = manifest.get("reproducibility", {}) if reproducibility.get("status") != "NOT_PERFORMED_BLOCKED_BEFORE_BUILD": errors.append("reproducibility must be recorded as not performed") for field in ("build_1_sha256", "build_2_sha256", "byte_identical"): if reproducibility.get(field) is not None: errors.append(f"reproducibility {field} must be null") return errors def validate_denylist(root: Path) -> list[str]: denylist = load_json(root / "manifests/artifact-denylist.json") for entry in denylist.get("entries", []): if ( entry.get("sha256") == "4be1c17b4964f2b68c39b5145bc4af4619c32512d60269ecf5c39728b390fa63" and entry.get("status") == "BLOCKED" and entry.get("permanent") is True and entry.get("execution_eligible") is False ): return [] return ["permanent blocked artifact is missing from the denylist"] def validate_repository_boundary(root: Path) -> list[str]: errors: list[str] = [] cmake = (root / "CMakeLists.txt").read_text(encoding="utf-8") forbidden_cmake = ( "CHIMERA_GFX_BUILD_PS5_OBSERVER", "chimera-gfx-bounded-observer", "phase09b-observer.elf", ) for value in forbidden_cmake: if value in cmake: errors.append(f"blocked observer target found in CMake: {value}") forbidden_paths = ( root / "samples/phase09b_observer", root / "samples/bounded_observer", root / "src/observer", root / "packaging/phase09b/observer.elf", ) for path in forbidden_paths: if path.exists(): errors.append(f"blocked observer source/artifact path exists: {path}") for base in (root / "samples", root / "src", root / "packaging"): for path in base.rglob("*"): if not path.is_file(): continue lowered = path.as_posix().lower() if path.suffix.lower() in {".c", ".cc", ".cpp", ".s", ".asm"} and ( "phase09b" in lowered or "bounded_observer" in lowered ): errors.append(f"blocked observer target source exists: {path}") if path.suffix.lower() in {".elf", ".self", ".sprx", ".pkg", ".map"} and ( "phase09b" in lowered or "observer" in lowered ): errors.append(f"blocked observer target artifact exists: {path}") try: tracked = git_output(root, "ls-files").splitlines() except (OSError, subprocess.CalledProcessError) as exc: return [f"could not inspect tracked files: {exc}"] for relative in tracked: lowered = relative.lower() if "phase09b" in lowered or "phase-0.9b" in lowered: if lowered.endswith((".elf", ".self", ".sprx", ".pkg", ".map")): errors.append(f"tracked Phase-0.9B target artifact exists: {relative}") if "phase09b" in lowered and ( "install" in lowered or "lifecycle-package" in lowered ): errors.append(f"Phase-0.9B install/lifecycle package exists: {relative}") return errors def validate_review_checksums(root: Path) -> list[str]: errors: list[str] = [] expected_paths = { "docs/approvals/phase-0.9b-observer-execution-template.md", "docs/runtime/phase-0.9b-bounded-observer-design.md", "docs/runtime/phase-0.9b-observer-limitations.md", "docs/runtime/phase-0.9b-observer-result-contract.md", "docs/runtime/phase-0.9b-observer-static-audit.md", "manifests/runtime/phase-0.9b-observation-plan.schema.json", "manifests/runtime/phase-0.9b-observer.json", "tests/phase09b_observer_model.py", "tests/test_phase09b_observer_audit.py", "tools/validate_phase09b_observer_audit.py", } checksum_path = root / "packaging/phase09b/SHA256SUMS.txt" observed: set[str] = set() for line_number, line in enumerate( checksum_path.read_text(encoding="utf-8").splitlines(), start=1 ): match = re.fullmatch(r"([0-9a-f]{64}) ([^\r\n]+)", line) if not match: errors.append(f"invalid checksum line {line_number}") continue expected_digest, relative = match.groups() if relative in observed: errors.append(f"duplicate checksum path: {relative}") continue observed.add(relative) path = root / relative if not path.is_file(): errors.append(f"checksummed file missing: {relative}") elif sha256_file(path) != expected_digest: errors.append(f"review checksum mismatch: {relative}") if observed != expected_paths: errors.append("Phase-0.9B review checksum inventory is incomplete or expanded") return errors def collect_errors(root: Path, require_source_trees: bool = True) -> list[str]: errors: list[str] = [] manifest_path = root / "manifests/runtime/phase-0.9b-observer.json" schema_path = root / "manifests/runtime/phase-0.9b-observation-plan.schema.json" required = [ manifest_path, schema_path, root / "docs/runtime/phase-0.9b-bounded-observer-design.md", root / "docs/runtime/phase-0.9b-observer-static-audit.md", root / "docs/runtime/phase-0.9b-observer-result-contract.md", root / "docs/runtime/phase-0.9b-observer-limitations.md", root / "docs/approvals/phase-0.9b-observer-execution-template.md", root / "tests/phase09b_observer_model.py", root / "tests/test_phase09b_observer_audit.py", root / "packaging/phase09b/SHA256SUMS.txt", ] for path in required: if not path.is_file(): errors.append(f"required Phase-0.9B file missing: {path}") if errors: return errors manifest = load_json(manifest_path) schema = load_json(schema_path) errors.extend(validate_manifest(root, manifest)) if not require_source_trees: errors = [ error for error in errors if not error.startswith(("source tree missing:", "source tree unreadable:")) ] errors.extend(validate_schema(schema, manifest)) errors.extend(validate_denylist(root)) errors.extend(validate_repository_boundary(root)) errors.extend(validate_review_checksums(root)) template = extract_json_contract( root / "docs/approvals/phase-0.9b-observer-execution-template.md", "PHASE09B_OBSERVER_EXECUTION_TEMPLATE", ) errors.extend(validate_authorizations(template, "execution_template")) if template.get("status") != "BLOCKED": errors.append("execution template must remain BLOCKED") required_fields = template.get("required_fields", {}) if any(value is not None for value in required_fields.values()): errors.append("execution template contains prefilled request-specific values") for relative in ( "docs/runtime/phase-0.9b-bounded-observer-design.md", "docs/runtime/phase-0.9b-observer-limitations.md", ): text = (root / relative).read_text(encoding="utf-8") normalized_text = re.sub(r"[^A-Z0-9]+", " ", text.upper()).strip() for decision in EXPECTED_DECISION: if decision.replace("_", " ") not in normalized_text: errors.append(f"{relative} does not state blocker {decision}") return errors def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) parser.add_argument( "--no-source-tree-check", action="store_true", help="Skip only missing external source-tree errors for packaged review.", ) args = parser.parse_args() root = args.root.resolve() errors = collect_errors(root, require_source_trees=not args.no_source_tree_check) if errors: for error in errors: print(f"ERROR: {error}", file=sys.stderr) return 1 print("Phase-0.9B blocked observer audit: PASS") return 0 if __name__ == "__main__": raise SystemExit(main())