railiance-platform/scripts/application_archive_retention_plan.py
codex a8fc1a3438
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Prepare immutable application archive transfer and retention admission
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11538-05ea-7b63-b3c7-a2df5f38898d
2026-10-07 23:23:33 +02:00

114 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Review primary archive generations; no provider or local deletion capability."""
from __future__ import annotations
import argparse
import json
import re
from datetime import UTC, datetime
from pathlib import Path
from primary_application_archive import PROFILES, require_version, validate_key
def identity(receipt):
return {
"object_key": receipt["object_key"],
"version_id": receipt["version_id"],
"ciphertext_sha256": receipt["ciphertext_sha256"],
"ciphertext_bytes": receipt["ciphertext_bytes"],
}
def plan(application, inventory, recovery, now=None):
now = now or datetime.now(UTC)
if (
recovery.get("status") != "application_restored_verified"
or recovery.get("application") != application
or recovery.get("application_restore_verified") is not True
):
raise ValueError("attended_application_recovery_proof_required")
protected = recovery.get("object_key", "")
validate_key(application, protected)
require_version(recovery.get("version_id"))
items, ignored = {}, []
for receipt in inventory:
key = receipt.get("object_key", "")
if (
receipt.get("application") != application
or receipt.get("status") != "offsite_fetched_verified"
or receipt.get("remote_retrieval_verified") is not True
or not isinstance(key, str)
or not key.startswith(PROFILES[application])
):
ignored.append(key)
continue
validate_key(application, key)
require_version(receipt.get("version_id"))
captured = datetime.fromisoformat(receipt["captured_at"].replace("Z", "+00:00"))
if (
captured.tzinfo is None
or captured > now
or key in items
or not re.fullmatch(r"[a-f0-9]{64}", receipt.get("ciphertext_sha256", ""))
or not isinstance(receipt.get("ciphertext_bytes"), int)
or receipt["ciphertext_bytes"] <= 0
):
raise ValueError("unverified_archive_inventory")
items[key] = (captured.astimezone(UTC), receipt)
if (
protected not in items
or items[protected][1]["version_id"] != recovery["version_id"]
or items[protected][1]["ciphertext_sha256"] != recovery.get("ciphertext_sha256")
):
raise ValueError("protected_recovery_point_missing_or_changed")
ordered = sorted(items, key=lambda key: (items[key][0], key), reverse=True)
daily, weekly = {}, {}
for key in ordered:
stamp = items[key][0]
daily.setdefault(stamp.date(), key)
weekly.setdefault(stamp.isocalendar()[:2], key)
keep = {protected, ordered[0]} | set(list(daily.values())[:14]) | set(list(weekly.values())[:4])
return {
"schema": "platform.application-retention-review.v1",
"status": "reviewed",
"application": application,
"daily_generations": 14,
"weekly_generations": 4,
"protected": identity(items[protected][1]),
"keep": [identity(items[key][1]) for key in ordered if key in keep],
"expiration_candidates": [identity(items[key][1]) for key in ordered if key not in keep],
"ignored": ignored,
"provider_mutations": False,
"local_mutations": False,
"activation_gate": "Separate owner must admit lifecycle/capacity, revalidate protected "
"recovery and exact versions before any expiration.",
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--application", choices=PROFILES, required=True)
parser.add_argument("--inventory", type=Path, required=True)
parser.add_argument("--recovery-receipt", type=Path, required=True)
args = parser.parse_args()
print(
json.dumps(
plan(
args.application,
json.loads(args.inventory.read_text()),
json.loads(args.recovery_receipt.read_text()),
),
indent=2,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as error:
print(json.dumps({"status": "failed", "failure_type": type(error).__name__}))
raise SystemExit(1)