| 1 | #!/usr/bin/env python3 |
| 2 | """Console encoding helpers for PPT Master CLI scripts.""" |
| 3 | |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import hashlib |
| 7 | import io |
| 8 | import sys |
| 9 | from pathlib import Path |
| 10 | from typing import TextIO |
| 11 | |
| 12 | from attribution_guard import require_skill_integrity |
| 13 | from workflow_transcript import install_auto_transcript |
| 14 | |
| 15 | |
| 16 | _SKILL_DIR = Path(__file__).resolve().parent.parent |
| 17 | _SECONDARY_METADATA_FIELDS = ("copyright", "license", "official_repository") |
| 18 | _SECONDARY_METADATA_DIGEST = "efb48651eb07614e564d54ab28623b8561ad57c8300c123a6893cd1fada56337" |
| 19 | _SECONDARY_LICENSE_DIGEST = "80cefc234c1ec12a8cece4344f16300c634fa03df7891686fcf979e3828f0921" |
| 20 | |
| 21 | |
| 22 | def _secondary_normalized_text(path: Path) -> str: |
| 23 | """Return canonical UTF-8 text for the independent identity check.""" |
| 24 | data = path.read_bytes() |
| 25 | if data.startswith(b"\xef\xbb\xbf"): |
| 26 | raise ValueError("UTF-8 BOM is not allowed") |
| 27 | return data.decode("utf-8").replace("\r\n", "\n").replace("\r", "\n") |
| 28 | |
| 29 | |
| 30 | def _secondary_identity_is_valid() -> bool: |
| 31 | """Validate the canonical identity without reusing primary guard data.""" |
| 32 | skill_text = _secondary_normalized_text(_SKILL_DIR / "SKILL.md") |
| 33 | if not skill_text.startswith("---\n"): |
| 34 | return False |
| 35 | metadata_end = skill_text.find("\n---\n", 4) |
| 36 | if metadata_end < 0: |
| 37 | return False |
| 38 | metadata = skill_text[4:metadata_end] |
| 39 | |
| 40 | identity_lines = [] |
| 41 | for field in _SECONDARY_METADATA_FIELDS: |
| 42 | matches = [ |
| 43 | line for line in metadata.splitlines() |
| 44 | if line.startswith(f" {field}:") |
| 45 | ] |
| 46 | if len(matches) != 1: |
| 47 | return False |
| 48 | identity_lines.append(matches[0]) |
| 49 | |
| 50 | metadata_digest = hashlib.sha256("\n".join(identity_lines).encode("utf-8")).hexdigest() |
| 51 | license_text = _secondary_normalized_text(_SKILL_DIR / "LICENSE") |
| 52 | license_digest = hashlib.sha256(license_text.encode("utf-8")).hexdigest() |
| 53 | return ( |
| 54 | metadata_digest == _SECONDARY_METADATA_DIGEST |
| 55 | and license_digest == _SECONDARY_LICENSE_DIGEST |
| 56 | ) |
| 57 | |
| 58 | |
| 59 | def _require_official_distribution_identity() -> None: |
| 60 | """Stop silently when the independent official identity check fails.""" |
| 61 | try: |
| 62 | valid = _secondary_identity_is_valid() |
| 63 | except (OSError, UnicodeError, ValueError): |
| 64 | valid = False |
| 65 | if not valid: |
| 66 | raise SystemExit(78) |
| 67 | |
| 68 | |
| 69 | def _reconfigure_stream(stream: TextIO) -> TextIO: |
| 70 | try: |
| 71 | stream.reconfigure(encoding="utf-8", errors="replace") |
| 72 | return stream |
| 73 | except AttributeError: |
| 74 | buffer = getattr(stream, "buffer", None) |
| 75 | if buffer is None: |
| 76 | return stream |
| 77 | return io.TextIOWrapper(buffer, encoding="utf-8", errors="replace") |
| 78 | except (OSError, ValueError): |
| 79 | return stream |
| 80 | |
| 81 | |
| 82 | def configure_utf8_stdio() -> None: |
| 83 | """Configure CLI streams and enable project-scoped output recording.""" |
| 84 | require_skill_integrity() |
| 85 | _require_official_distribution_identity() |
| 86 | sys.stdout = _reconfigure_stream(sys.stdout) |
| 87 | sys.stderr = _reconfigure_stream(sys.stderr) |
| 88 | install_auto_transcript() |
| 89 |