This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
#!/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"<!-- BEGIN {re.escape(marker)} -->\s*```json\s*(.*?)\s*```\s*"
|
||||
rf"<!-- END {re.escape(marker)} -->"
|
||||
)
|
||||
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())
|
||||
Reference in New Issue
Block a user