#!/usr/bin/env python3 """Automatically accept an exact release binding under an ACTIVE campaign.""" from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import stat import tempfile from typing import Any, Mapping from release_campaign_authorization import CampaignAuthorization, CampaignError class AcceptanceError(RuntimeError): pass def secure_json(path: Path) -> dict[str, Any]: if path.is_symlink() or not path.is_file(): raise AcceptanceError(f"invalid_file:{path}") st = path.lstat() if st.st_uid != 0 or stat.S_IMODE(st.st_mode) & 0o022: raise AcceptanceError(f"unsafe_file:{path}") value = json.loads(path.read_text(encoding="utf-8")) if not isinstance(value, dict): raise AcceptanceError(f"json_object_required:{path}") return value def atomic_new(path: Path, value: Mapping[str, Any]) -> None: if path.exists() or path.is_symlink(): raise AcceptanceError("acceptance_receipt_exists") path.parent.mkdir(parents=True, exist_ok=True) fd, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) try: os.fchmod(fd, 0o600) if os.geteuid() == 0: os.fchown(fd, 0, 0) with os.fdopen(fd, "w", encoding="utf-8", closefd=True) as stream: json.dump(dict(value), stream, sort_keys=True, indent=2) stream.write("\n") stream.flush() os.fsync(stream.fileno()) os.replace(name, path) os.chmod(path, 0o600) finally: try: os.unlink(name) except FileNotFoundError: pass def evaluate( campaign: CampaignAuthorization, binding: Mapping[str, Any], evidence: Mapping[str, Any], artifact: Mapping[str, Any], staging: Mapping[str, Any], main: Mapping[str, Any], tag: Mapping[str, Any], ) -> dict[str, Any]: required_binding = { "version", "target_oid", "evidence_closure_oid", "artifact_sha256", "artifact_size", "approval_plan_id", } if set(binding) != required_binding: raise AcceptanceError("binding_fields_invalid") if main.get("repository") != "aios/aios": raise AcceptanceError("repository_identity_mismatch") if main.get("target_in_main_lineage") is not True: raise AcceptanceError("target_outside_main_lineage") if evidence.get("object_type") != "commit": raise AcceptanceError("evidence_not_commit") if evidence.get("target_is_ancestor") is not True: raise AcceptanceError("target_not_evidence_ancestor") if evidence.get("closure_oid") != binding["evidence_closure_oid"]: raise AcceptanceError("evidence_oid_mismatch") if artifact.get("sha256") != binding["artifact_sha256"]: raise AcceptanceError("artifact_digest_mismatch") if artifact.get("size") != binding["artifact_size"]: raise AcceptanceError("artifact_size_mismatch") if artifact.get("retained") is not True: raise AcceptanceError("artifact_not_retained") if staging.get("install") != "PASS": raise AcceptanceError("staging_install_failed") if staging.get("health") != "PASS": raise AcceptanceError("staging_health_failed") if staging.get("restart") != "PASS": raise AcceptanceError("staging_restart_failed") if staging.get("rollback_readiness") != "PASS": raise AcceptanceError("rollback_readiness_failed") if tag.get("conflict") is not False: raise AcceptanceError("tag_conflict") campaign.assert_operation("derive_campaign_acceptance") return { "schema": "aios.campaign-release-acceptance.v1", "accepted": True, "binding_replaced": False, "campaign_id": campaign.campaign_id, "campaign_generation": campaign.data["generation"], **dict(binding), "owner_interaction_required": False, } def main() -> int: p = argparse.ArgumentParser() p.add_argument("--campaign", type=Path, required=True) p.add_argument("--revocation", type=Path) p.add_argument("--binding", type=Path, required=True) p.add_argument("--evidence", type=Path, required=True) p.add_argument("--artifact", type=Path, required=True) p.add_argument("--staging", type=Path, required=True) p.add_argument("--main-facts", type=Path, required=True) p.add_argument("--tag-facts", type=Path, required=True) p.add_argument("--output", type=Path, required=True) a = p.parse_args() campaign = CampaignAuthorization.load( a.campaign, revocation=a.revocation, require_active=True, ) receipt = evaluate( campaign, secure_json(a.binding), secure_json(a.evidence), secure_json(a.artifact), secure_json(a.staging), secure_json(a.main_facts), secure_json(a.tag_facts), ) atomic_new(a.output, receipt) print(json.dumps(receipt, sort_keys=True)) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (CampaignError, AcceptanceError) as exc: print(json.dumps({"status": "BLOCKED", "reason": str(exc)}, sort_keys=True)) raise SystemExit(2)