generate_p3_wp14_verification_ledger.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """Run bounded local P3-WP14 verification and write a machine-readable ledger.
  2. The ledger intentionally stores digests of captured stdout/stderr, not their
  3. contents, so it carries no service configuration or credentials. It neither
  4. starts Compose nor contacts an enterprise system; the config validator performs
  5. only a local Compose render.
  6. """
  7. from __future__ import annotations
  8. import hashlib
  9. import json
  10. import os
  11. import re
  12. import subprocess
  13. import sys
  14. from datetime import datetime, timedelta, timezone
  15. from pathlib import Path
  16. from p3_wp14_secure_io import atomic_write_bytes, exclusive_lock, read_bytes_once, read_json_once
  17. ROOT = Path(__file__).resolve().parents[1]
  18. COMMAND_HOME = ROOT / "work/p3_wp14_ledger_command_home"
  19. PYTHON = ROOT / ".venv/bin/python"
  20. LEDGER = ROOT / "docs/validation/P3_WP14_LOCAL_VERIFICATION_LEDGER.json"
  21. LOG = ROOT / "docs/validation/P3_WP14_LOCAL_VERIFICATION.log"
  22. TRACE = ROOT / "docs/phase3/P3_WP14_RELEASE_TRACEABILITY.json"
  23. MANIFEST = ROOT / "docs/phase3/P3_WP14_RELEASE_MANIFEST.json"
  24. STATE = ROOT / "docs/validation/P3_WP14_VERIFICATION_RUN_STATE.json"
  25. LOCK = ROOT / "work/p3_wp14_ledger_lock_scope/lock"
  26. TTL_SECONDS = 900
  27. INPUT_PATHS = (
  28. "docs/phase3/P3_WP14_RELEASE_MANIFEST.json",
  29. "docs/validation/P3_WP14_LOCAL_CONFIG_A.json",
  30. "docs/validation/P3_WP14_LOCAL_CONFIG_B.json",
  31. "docs/validation/P3_WP14_LOCAL_CONFIG_COMPARISON.json",
  32. "docs/acceptance/P3_WP14_UAT_CASES.json",
  33. "tests/test_phase3_wp14_acceptance_handover_contract.py",
  34. "scripts/validate_p3_wp14_local_configs.py",
  35. "scripts/generate_p3_wp14_release_manifest.py",
  36. "scripts/generate_p3_wp14_verification_ledger.py",
  37. "scripts/verify_p3_wp14_fresh_local_engineering.py",
  38. "scripts/p3_wp14_secure_io.py",
  39. )
  40. TARGETED_TESTS = (
  41. "tests/test_phase3_wp01_enterprise_acceptance_contract.py",
  42. "tests/test_phase3_wp02_enterprise_identity_contract.py",
  43. "tests/test_phase3_wp03_enterprise_connectors.py",
  44. "tests/test_phase3_wp04_delivery_contract.py",
  45. "tests/test_phase3_wp04_production_runtime.py",
  46. "tests/core/orchestration/test_production_observability.py",
  47. "tests/test_phase3_wp06_subscription_migration_contract.py",
  48. "tests/test_phase3_wp07_enterprise_delivery_contract.py",
  49. "tests/acceptance/test_phase3_wp08_third_domain_replication.py",
  50. "tests/agent/test_wp09_model_gateway.py",
  51. "tests/test_wp10_tenant_api.py",
  52. "tests/test_wp11_bi_ai_catalog_api.py",
  53. "tests/test_wp12_metering_showback_api.py",
  54. "tests/test_wp13_plugin_platform_api.py",
  55. "tests/test_permission_matrix.py",
  56. )
  57. def _sha256_bytes(value: bytes) -> str:
  58. return hashlib.sha256(value).hexdigest()
  59. def _sha256_path(path: Path) -> str:
  60. return _sha256_bytes(read_bytes_once(path))
  61. def ledger_is_fresh(payload: dict, now: datetime | None = None) -> bool:
  62. current = now or datetime.now(timezone.utc)
  63. return datetime.fromisoformat(payload["expires_at"].replace("Z", "+00:00")) > current
  64. def _write_json(path: Path, value: object) -> None:
  65. atomic_write_bytes(path, json.dumps(value, ensure_ascii=False, indent=2).encode("utf-8") + b"\n")
  66. def _sanitized(value: str) -> str:
  67. return re.sub(r"(?i)(password|token|secret|api[_-]?key)\s*[:=]\s*\S+", r"\1=<REDACTED>", value)
  68. def _entry_path(command: list[str], dependencies: list[Path]) -> Path:
  69. """The entry is the first repository file explicitly passed to the command."""
  70. for part in command[1:]:
  71. path = ROOT / part
  72. if path.is_file():
  73. return path
  74. return dependencies[0]
  75. def _minimal_command_env() -> dict[str, str]:
  76. """Run bounded local commands without inherited credential/config paths."""
  77. return {
  78. "PATH": os.environ.get("PATH", ""),
  79. "HOME": str(COMMAND_HOME),
  80. "TMPDIR": str(COMMAND_HOME / "tmp"),
  81. "DOCKER_CONFIG": str(COMMAND_HOME / "docker"),
  82. "PYTHONDONTWRITEBYTECODE": "1",
  83. "LANG": "C.UTF-8",
  84. "LC_ALL": "C.UTF-8",
  85. }
  86. def _run(command_id: str, command: list[str], case_ids: list[str]) -> tuple[dict, str]:
  87. completed = subprocess.run(
  88. command,
  89. cwd=ROOT,
  90. env=_minimal_command_env(),
  91. text=True,
  92. capture_output=True,
  93. check=False,
  94. timeout=300,
  95. )
  96. output = completed.stdout + completed.stderr
  97. sanitized_output = _sanitized(output)
  98. match = re.search(r"(\d+) passed", output)
  99. dependencies = [ROOT / part for part in command[1:] if (ROOT / part).is_file()]
  100. if command_id == "p3_targeted_regression": dependencies.extend(ROOT / target for target in TARGETED_TESTS)
  101. dependencies.append(ROOT / "scripts/p3_wp14_secure_io.py")
  102. dependencies = sorted(set(dependencies))
  103. dependency_digests = {path.relative_to(ROOT).as_posix(): _sha256_path(path) for path in dependencies}
  104. run = {
  105. "command_id": command_id,
  106. "command": command,
  107. "case_ids": case_ids,
  108. "run_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
  109. "status": "PASS" if completed.returncode == 0 else "FAIL",
  110. "exit_code": completed.returncode,
  111. "passed_count": int(match.group(1)) if match else 1 if completed.returncode == 0 else 0,
  112. "timeout_seconds": 300,
  113. "cwd": ".",
  114. "command_sha256": _sha256_bytes(json.dumps(command, separators=(",", ":")).encode("utf-8")),
  115. "entry_path": _entry_path(command, dependencies).relative_to(ROOT).as_posix(),
  116. "script_sha256": _sha256_path(_entry_path(command, dependencies)),
  117. "dependency_sha256": dependency_digests,
  118. "dependency_aggregate_sha256": _sha256_bytes(json.dumps(dependency_digests, sort_keys=True, separators=(",", ":")).encode("utf-8")),
  119. "stdout_sha256": _sha256_bytes(_sanitized(completed.stdout).encode("utf-8")),
  120. "stderr_sha256": _sha256_bytes(_sanitized(completed.stderr).encode("utf-8")),
  121. }
  122. return run, f"{command_id} exit={completed.returncode} passed={run['passed_count']} output_sha256={_sha256_bytes(sanitized_output.encode('utf-8'))}"
  123. def main() -> int:
  124. with exclusive_lock(LOCK) as nonce:
  125. now = datetime.now(timezone.utc).replace(microsecond=0)
  126. _write_json(STATE, {
  127. "schema_version": "1.0", "status": "RUNNING", "run_nonce": nonce,
  128. "started_at": now.isoformat().replace("+00:00", "Z"),
  129. "expires_at": (now + timedelta(seconds=TTL_SECONDS)).isoformat().replace("+00:00", "Z"),
  130. })
  131. try:
  132. return _main_locked(nonce, now)
  133. except (subprocess.TimeoutExpired, Exception) as error:
  134. _write_json(STATE, {"schema_version": "1.0", "status": "FAILED", "run_nonce": nonce, "error_type": type(error).__name__, "invalidates_prior_ledger_and_trace": True})
  135. return 1
  136. def _main_locked(nonce: str, now: datetime) -> int:
  137. commands = (
  138. (
  139. "p3_targeted_regression",
  140. [str(PYTHON), "-m", "pytest", "-q", *TARGETED_TESTS],
  141. [f"P3-WP14-UAT-{number:02d}" for number in range(1, 14)] + ["P3-WP14-UAT-23", "P3-WP14-UAT-25", "P3-WP14-UAT-27"],
  142. ),
  143. (
  144. "local_config_compose_render",
  145. [str(PYTHON), "scripts/validate_p3_wp14_local_configs.py"],
  146. ["P3-WP14-UAT-30"],
  147. ),
  148. (
  149. "release_manifest_generation",
  150. [str(PYTHON), "scripts/generate_p3_wp14_release_manifest.py"],
  151. ["P3-WP14-UAT-29"],
  152. ),
  153. (
  154. "wp14_264_audit_contract",
  155. [str(PYTHON), "-m", "pytest", "-q", "tests/test_phase3_wp14_acceptance_handover_contract.py", "-k", "264_item_closure"],
  156. ["P3-WP14-UAT-26"],
  157. ),
  158. (
  159. "wp14_browser_boundary_contract",
  160. [str(PYTHON), "-m", "pytest", "-q", "tests/test_phase3_wp14_acceptance_handover_contract.py", "-k", "report_ledger_signoff_runbook"],
  161. ["P3-WP14-UAT-28"],
  162. ),
  163. )
  164. runs: list[dict] = []
  165. log_sections: list[str] = []
  166. for command_id, command, case_ids in commands:
  167. run, output = _run(command_id, command, case_ids)
  168. runs.append(run)
  169. log_sections.append(output)
  170. if run["exit_code"] != 0:
  171. atomic_write_bytes(LOG, ("\n".join(log_sections) + "\n").encode("utf-8"))
  172. _write_json(STATE, {"schema_version": "1.0", "status": "FAILED", "run_nonce": nonce, "failed_command_id": command_id})
  173. return run["exit_code"]
  174. atomic_write_bytes(LOG, ("\n".join(log_sections) + "\n").encode("utf-8"))
  175. controlled = {path: _sha256_path(ROOT / path) for path in INPUT_PATHS}
  176. payload = {
  177. "schema_version": "1.0",
  178. "work_package": "P3-WP14",
  179. "cache_class": "UNSIGNED_REPRODUCIBILITY_CACHE",
  180. "pass_local_authority": "FRESH_VERIFIER_CURRENT_PROCESS_ONLY",
  181. "external_signature_anchor": {"status": "TBD_EXTERNAL", "gate": "BLOCKED_EXTERNAL"},
  182. "generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
  183. "expires_at": (now + timedelta(seconds=TTL_SECONDS)).isoformat().replace("+00:00", "Z"),
  184. "run_nonce": nonce,
  185. "security_envelope": {
  186. "locked": True,
  187. "atomic_write": True,
  188. "fd_single_read": True,
  189. "minimal_environment": True,
  190. "stdout_stderr_persisted": "SANITIZED_DIGEST_ONLY",
  191. },
  192. "controlled_input_paths": list(INPUT_PATHS),
  193. "controlled_input_sha256": _sha256_bytes(
  194. json.dumps(controlled, sort_keys=True, separators=(",", ":")).encode("utf-8")
  195. ),
  196. "log_path": "docs/validation/P3_WP14_LOCAL_VERIFICATION.log",
  197. "log_sha256": _sha256_path(LOG),
  198. "runs": runs,
  199. "boundary": "This file and its log/trace are unsigned reproducibility caches and cannot independently grant PASS_LOCAL. Only the fresh verifier may report a current-process local result. All commands are local engineering checks; Compose is rendered only; no deployment, enterprise endpoint, or enterprise UAT is performed.",
  200. }
  201. _write_json(LEDGER, payload)
  202. trace = read_json_once(TRACE)
  203. assert isinstance(trace, dict)
  204. trace["release_manifest_path"] = "docs/phase3/P3_WP14_RELEASE_MANIFEST.json"
  205. trace["release_manifest_sha256"] = _sha256_path(MANIFEST)
  206. trace["verification_ledger_sha256"] = _sha256_path(LEDGER)
  207. trace["verification_log_sha256"] = _sha256_path(LOG)
  208. trace["local_verification_cache"] = {
  209. "classification": "UNSIGNED_REPRODUCIBILITY_CACHE",
  210. "pass_local_authority": "FRESH_VERIFIER_CURRENT_PROCESS_ONLY",
  211. "external_signature_status": "TBD_EXTERNAL",
  212. "enterprise_gate": "BLOCKED_EXTERNAL",
  213. }
  214. trace["second_isolated_local_environment"] = {
  215. "formal_same_signed_version_status": "BLOCKED_EXTERNAL",
  216. "formal_same_signed_version_reason": "manifest is unsigned; signature and enterprise second-environment authority are TBD_EXTERNAL",
  217. "local_portability_rehearsal": {
  218. "result": "PASS_LOCAL",
  219. "manifest_sha256": trace["release_manifest_sha256"],
  220. "manifest_signed": False,
  221. "boundary": "Two closed, non-secret local configuration renders are bound to this unsigned manifest digest. This is portability rehearsal only, not the required same signed-version second isolated environment.",
  222. },
  223. }
  224. _write_json(TRACE, trace)
  225. _write_json(STATE, {
  226. "schema_version": "1.0", "status": "COMPLETE", "run_nonce": nonce,
  227. "generated_at": payload["generated_at"], "expires_at": payload["expires_at"],
  228. })
  229. return 0
  230. if __name__ == "__main__":
  231. sys.exit(main())