#!/usr/bin/env python3 """Build a redacted live credential handoff report.""" from __future__ import annotations import argparse import json import os from pathlib import Path from typing import Any from proofframe.config import normalize_genblaze_provider ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "docs" / "assets" / "live-credential-handoff.json" DEFAULT_MD = ROOT / "docs" / "assets" / "live-credential-handoff.md" PLACEHOLDERS = {"", "placeholder", "change-me", "changeme", "todo", "tbd", "none", "null", "..."} REQUIRED_GROUPS = [ { "id": "storage_backend_mode", "label": "ProofFrame storage backend mode", "accepted_names": ["PROOFFRAME_STORAGE_BACKEND"], "expected": "b2", "remediation": "Set PROOFFRAME_STORAGE_BACKEND=b2.", }, { "id": "generation_backend_mode", "label": "ProofFrame generation backend mode", "accepted_names": ["PROOFFRAME_GENERATION_BACKEND"], "expected": "genblaze", "remediation": "Set PROOFFRAME_GENERATION_BACKEND=genblaze.", }, { "id": "b2_endpoint", "label": "Backblaze B2 S3 endpoint", "accepted_names": ["B2_ENDPOINT_URL", "B2_S3_ENDPOINT_URL"], "remediation": "Set B2_ENDPOINT_URL from the Backblaze bucket S3 endpoint.", }, { "id": "b2_bucket", "label": "Backblaze B2 bucket", "accepted_names": ["B2_BUCKET"], "remediation": "Set B2_BUCKET to the dedicated demo bucket name.", }, { "id": "b2_key_id", "label": "Backblaze B2 key id", "accepted_names": ["B2_KEY_ID"], "remediation": "Set B2_KEY_ID for a least-privilege application key.", }, { "id": "b2_application_key", "label": "Backblaze B2 application key", "accepted_names": ["B2_APPLICATION_KEY", "B2_APP_KEY"], "remediation": "Set B2_APPLICATION_KEY or B2_APP_KEY.", }, { "id": "genblaze_provider_key", "label": "Genblaze provider API key", "accepted_names_by_provider": { "gmicloud": ["GENBLAZE_API_KEY", "GMI_API_KEY"], "openai": ["OPENAI_API_KEY"], "local": [], }, "remediation_by_provider": { "gmicloud": "Set GENBLAZE_API_KEY or GMI_API_KEY.", "openai": "Set OPENAI_API_KEY.", "local": "GENBLAZE_PROVIDER=local does not require a provider API key.", }, }, { "id": "genblaze_image_model", "label": "Genblaze image model", "accepted_names": ["GENBLAZE_IMAGE_MODEL"], "remediation": "Set GENBLAZE_IMAGE_MODEL to the verified image model.", }, ] OPTIONAL_GROUPS = [ { "id": "b2_public_base_url", "label": "B2 public base URL", "accepted_names": ["B2_PUBLIC_BASE_URL"], }, { "id": "b2_region", "label": "B2 region for Genblaze sink", "accepted_names": ["B2_REGION"], }, { "id": "genblaze_provider", "label": "Genblaze provider", "accepted_names": ["GENBLAZE_PROVIDER"], }, { "id": "genblaze_aspect_ratio", "label": "Genblaze aspect ratio", "accepted_names": ["GENBLAZE_ASPECT_RATIO"], }, { "id": "genblaze_timeout", "label": "Genblaze timeout seconds", "accepted_names": ["GENBLAZE_TIMEOUT_SECONDS"], }, ] NEXT_COMMANDS = [ "python scripts/final_env_wizard.py --prefill-non-secret --output .env.final.local", "python scripts/final_env_wizard.py --output .env.final.local --missing-only --force", "python scripts/live_env_handoff.py --env-file .env.final.local", "python scripts/run_final_live_proof.py --env-file .env.final.local --preflight-only", "python scripts/run_final_live_proof.py --env-file .env.final.local --genblaze-provider local --genblaze-image-model local-svg-v1 --preflight-only", "python scripts/run_final_live_proof.py --env-file .env.final.local --genblaze-provider openai --genblaze-image-model gpt-image-1 --preflight-only", "python scripts/run_final_live_proof.py --env-file .env.final.local --evidence-out docs/assets/final-live-proof-evidence.json", 'python scripts/devpost_packet.py --post-live --video-url "$PROOFFRAME_PUBLIC_VIDEO_URL"', "python scripts/secret_scan.py", "python scripts/claim_lint.py", "python scripts/submission_audit.py --strict-final", ] def normalize_value(value: str | None) -> str: if value is None: return "" return value.strip().strip('"').strip("'") def has_real_value(value: str | None) -> bool: normalized = normalize_value(value) if normalized.lower() in PLACEHOLDERS: return False if normalized.startswith("<") and normalized.endswith(">"): return False return bool(normalized) def parse_env_file(path: Path) -> dict[str, str]: values: dict[str, str] = {} for raw_line in path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() if key.startswith("export "): key = key.removeprefix("export ").strip() if key: values[key] = normalize_value(value) return values def env_source(env_file: Path | None) -> tuple[dict[str, str], str]: values = {key: value for key, value in os.environ.items()} source = "process environment" if env_file: file_values = parse_env_file(env_file) values.update(file_values) source = str(env_file) return values, source def evaluate_group(group: dict[str, Any], values: dict[str, str]) -> dict[str, Any]: provider = normalize_genblaze_provider(values.get("GENBLAZE_PROVIDER", "gmicloud")) accepted_names = group.get("accepted_names") remediation = group.get("remediation", "") if "accepted_names_by_provider" in group: accepted_by_provider = group["accepted_names_by_provider"] accepted_names = accepted_by_provider.get( provider, sorted({name for names in accepted_by_provider.values() for name in names}), ) remediation = group.get("remediation_by_provider", {}).get( provider, "Set GENBLAZE_PROVIDER to gmicloud, openai, or local and provide its required setup.", ) if provider == "local" and not accepted_names: return { "id": group["id"], "label": group["label"], "ok": True, "accepted_names": accepted_names, "present_names": [], "expected": group.get("expected"), "remediation": "", } present_names = [name for name in accepted_names if has_real_value(values.get(name))] expected = group.get("expected") expected_ok = True if expected is not None: expected_ok = any(normalize_value(values.get(name)).lower() == expected for name in present_names) ok = bool(present_names) and expected_ok return { "id": group["id"], "label": group["label"], "ok": ok, "accepted_names": accepted_names, "present_names": present_names, "expected": expected, "remediation": "" if ok else remediation, } def build_report(env_file: Path | None = None) -> dict[str, Any]: values, source = env_source(env_file) required = [evaluate_group(group, values) for group in REQUIRED_GROUPS] optional = [evaluate_group(group, values) for group in OPTIONAL_GROUPS] missing = [item for item in required if not item["ok"]] return { "schema": "proofframe.live_credential_handoff.v1", "ok": not missing, "mode": "live_env_ready" if not missing else "missing_live_env", "source": source, "required": required, "optional": optional, "missing_ids": [item["id"] for item in missing], "next_commands": NEXT_COMMANDS, "secret_policy": ( "This report records only variable names and presence checks. It never prints, hashes, " "stores, or commits credential values." ), } def render_markdown(report: dict[str, Any]) -> str: lines = [ "# ProofFrame Live Credential Handoff", "", f"Mode: `{report['mode']}`", f"Ready for live proof: `{str(report['ok']).lower()}`", f"Source: `{report['source']}`", "", report["secret_policy"], "", "## Required Values", "", ] for item in report["required"]: marker = "OK" if item["ok"] else "MISSING" names = ", ".join(f"`{name}`" for name in item["accepted_names"]) present = ", ".join(f"`{name}`" for name in item["present_names"]) or "none" expected = f"; expected `{item['expected']}`" if item["expected"] else "" lines.append(f"- {marker} {item['label']}: {names}; present {present}{expected}.") if item["remediation"]: lines.append(f" Remediation: {item['remediation']}") lines.extend(["", "## Optional Values", ""]) for item in report["optional"]: marker = "SET" if item["present_names"] else "UNSET" names = ", ".join(f"`{name}`" for name in item["accepted_names"]) present = ", ".join(f"`{name}`" for name in item["present_names"]) or "none" lines.append(f"- {marker} {item['label']}: {names}; present {present}.") lines.extend(["", "## Next Commands", ""]) lines.extend(f"```bash\n{command}\n```" for command in report["next_commands"]) return "\n".join(lines) + "\n" def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None: json_path.parent.mkdir(parents=True, exist_ok=True) markdown_path.parent.mkdir(parents=True, exist_ok=True) json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") markdown_path.write_text(render_markdown(report), encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Build a redacted ProofFrame live credential handoff report." ) parser.add_argument("--env-file", type=Path, help="Optional local env file; values are not printed.") parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) parser.add_argument( "--strict", action="store_true", help="Exit nonzero when required live variables are missing.", ) return parser def main() -> None: args = build_parser().parse_args() report = build_report(args.env_file) write_outputs(report, args.json_out, args.markdown_out) print( json.dumps( { "ok": report["ok"], "mode": report["mode"], "json": str(args.json_out), "markdown": str(args.markdown_out), "missing_ids": report["missing_ids"], }, indent=2, ) ) if args.strict and not report["ok"]: raise SystemExit(2) if __name__ == "__main__": main()