"""Durable image-release coordinator; production adapters are deliberately separate. Trust keys and role bindings come from operator-owned configuration, never requests. Adapters must implement the fixed Git/ArgoCD contract documented in release admission. No credential loading, generic command execution, HTTP endpoint or live adapter here. """ from __future__ import annotations import base64 import hashlib import json import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Protocol from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from .gitops_policy import validate ROLES = {"build", "review", "health", "retention"} AUTHORITY = "ACTIVITY-WP-0041-image-only-v1" TERMINAL = {"complete", "rolled_back", "cancelled"} REQUIRED_CHECKS = {"CI Smoke / host-smoke (push)", "CI Smoke / container-smoke (push)", "Build and Publish Container Image / build-and-push (push)"} def canonical(value): return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() def fingerprint(value): return hashlib.sha256(canonical(value)).hexdigest() def timestamp(value): result = datetime.fromisoformat(value.replace("Z", "+00:00")) if result.tzinfo is None: raise ValueError("timezone required") return result class Receipts: """Verify Ed25519 envelopes against locally bound principals and roles.""" def __init__(self, trusted): # key id -> {public_key: raw 32 bytes, principal: str, roles: set[str]} self.trusted = trusted def verify(self, envelope, role, binding, now): if set(envelope) != {"key_id", "payload", "signature"}: raise ValueError("unexpected envelope fields") trust = self.trusted.get(envelope["key_id"]) if trust is None or role not in trust["roles"]: raise ValueError("untrusted receipt role") payload = envelope["payload"] signature = base64.b64decode(envelope["signature"], validate=True) Ed25519PublicKey.from_public_bytes(trust["public_key"]).verify(signature, canonical(payload)) if (payload.get("role") != role or payload.get("schema") != 1 or payload.get("authority") != AUTHORITY or payload.get("binding") != binding or payload.get("result") != "pass"): raise ValueError("receipt scope/result mismatch") issued, expires = timestamp(payload["issued_at"]), timestamp(payload["expires_at"]) if not now - timedelta(minutes=5) <= issued <= now < expires <= issued + timedelta(minutes=5): raise ValueError("stale or future receipt") return trust["principal"], payload class Backend(Protocol): """Trusted adapter, never implemented from caller-provided URLs or commands. publish_revision is a CAS on the single platform child targetRevision field; already-at-target is success, any third revision is a conflict. It must retain source signatures/checks, serialize with other writers and return only after durable Git publication. sync_revision selects ONLY the activity-core child in the root, then ONLY the child application, with prune false and no overrides. Both methods must be idempotent after a lost response or broker restart. healthy includes exact revision, deployment health, report/schedule invariants. """ def publish_revision(self, expected: str, target: str) -> None: ... def sync_revision(self, target: str) -> None: ... def healthy(self, target: str) -> bool: ... class Broker: def __init__(self, database: Path, receipts: Receipts, *, admitted=False): self.database, self.receipts, self.admitted = database, receipts, admitted with self.connect() as db: db.execute("CREATE TABLE IF NOT EXISTS releases (id TEXT PRIMARY KEY, plan TEXT NOT NULL, phase TEXT NOT NULL, active INTEGER UNIQUE, deadline TEXT NOT NULL, error TEXT)") db.execute("CREATE TABLE IF NOT EXISTS transitions (sequence INTEGER PRIMARY KEY, release_id TEXT NOT NULL, phase TEXT NOT NULL, observed_at TEXT NOT NULL)") def connect(self): db = sqlite3.connect(self.database, timeout=5, isolation_level=None) db.execute("PRAGMA synchronous=FULL") return db def admit(self, before, after, candidate, rollback, envelopes, now=None): if not self.admitted: raise ValueError("broker identity not admitted") now = now or datetime.now(timezone.utc) binding = {"repository": "coulomb/activity-core", "application": "activity-core", "candidate": candidate, "rollback": rollback, "before_sha256": fingerprint(before), "after_sha256": fingerprint(after)} if set(envelopes) != ROLES: raise ValueError("all independent receipt roles required") verified = {r: self.receipts.verify(envelopes[r], r, binding, now) for r in ROLES} if (verified["build"][0] == verified["review"][0] or self.receipts.trusted[envelopes["build"]["key_id"]]["public_key"] == self.receipts.trusted[envelopes["review"]["key_id"]]["public_key"]): raise ValueError("reviewer must be independent of producer") health = verified["health"][1] # A signed continuous observation attestation must name the prior revision. if health.get("revision") != rollback or health.get("continuous") is not True: raise ValueError("continuous prior-revision observation required") if health.get("synced") is not True or health.get("healthy") is not True: raise ValueError("prior revision unhealthy") evidence = dict(schema_version=1, identity_admitted=True, authority=AUTHORITY, checks="pass", review_result="pass", producer=verified["build"][0], reviewer=verified["review"][0], candidate_commit=candidate, rollback_commit=rollback, healthy_since=health["healthy_since"], observed_at=health["issued_at"], argo_synced=True, argo_healthy=True) validate(before, after, evidence, now) required = sorted({c["image"] for docs in (before, after) for d in docs if d["kind"] == "Deployment" for c in d["spec"]["template"]["spec"]["containers"]}) if sorted(verified["retention"][1].get("images", [])) != required: raise ValueError("live and rollback image retention coverage required") checks = verified["build"][1].get("checks", {}) if not isinstance(checks, dict) or any(checks.get(name) != "success" for name in REQUIRED_CHECKS): raise ValueError("required build and smoke checks missing") # Signed build receipt binds each exact candidate digest, not just a green status. candidate_images = sorted({c["image"] for d in after if d["kind"] == "Deployment" for c in d["spec"]["template"]["spec"]["containers"]}) if sorted(verified["build"][1].get("images", [])) != candidate_images: raise ValueError("build digest binding missing") plan = {**binding, "receipts": envelopes, "receipt_hashes": {r: fingerprint(envelopes[r]) for r in ROLES}} release_id = fingerprint(binding) db = self.connect() try: db.execute("BEGIN IMMEDIATE") existing = db.execute("SELECT id FROM releases WHERE id=?", (release_id,)).fetchone() if existing: db.rollback() return release_id db.execute("INSERT INTO releases VALUES (?, ?, 'planned', 1, ?, NULL)", (release_id, canonical(plan).decode(), min(timestamp(verified[r][1]["expires_at"]) for r in ROLES).isoformat())) db.execute("INSERT INTO transitions(release_id,phase,observed_at) VALUES (?, 'planned', ?)", (release_id, now.isoformat())) db.commit() finally: db.close() return release_id def advance(self, release_id, backend: Backend, now=None): """One durable step. Adapter exceptions preserve intent for safe retry. Failure/timeout after publication moves to rollback; rollback failure keeps the unique active slot, halting all new releases until recovery succeeds. A database write lock serializes adapter calls across broker processes. """ if not self.admitted: raise ValueError("broker identity not admitted") now = now or datetime.now(timezone.utc) db = self.connect() try: db.execute("BEGIN IMMEDIATE") row = db.execute("SELECT plan, phase, deadline FROM releases WHERE id=?", (release_id,)).fetchone() if row is None: raise ValueError("unknown release") plan, phase, deadline = json.loads(row[0]), row[1], timestamp(row[2]) candidate, rollback = plan["candidate"], plan["rollback"] if phase in TERMINAL: db.rollback() return phase next_phase = phase if phase == "planned": if now >= deadline: # No publication attempted; safe terminal cancellation. next_phase = "cancelled" else: # Persist intent BEFORE the external CAS; a lost response # must never be mistaken for an unpublished cancellation. next_phase = "publish_pending" elif phase == "publish_pending": if now >= deadline: next_phase = "rollback_planned" else: backend.publish_revision(rollback, candidate) next_phase = "published" elif phase == "published": if now >= deadline: next_phase = "rollback_planned" else: backend.sync_revision(candidate) next_phase = "synced" elif phase == "synced": next_phase = "complete" if now < deadline and backend.healthy(candidate) is True else "rollback_planned" elif phase == "rollback_planned": backend.publish_revision(candidate, rollback) next_phase = "rollback_published" elif phase == "rollback_published": backend.sync_revision(rollback) next_phase = "rollback_synced" elif phase == "rollback_synced": if backend.healthy(rollback) is True: next_phase = "rolled_back" else: raise ValueError("unknown durable phase") db.execute("UPDATE releases SET phase=?, active=? WHERE id=?", (next_phase, None if next_phase in TERMINAL else 1, release_id)) if next_phase != phase: db.execute("INSERT INTO transitions(release_id,phase,observed_at) VALUES (?, ?, ?)", (release_id, next_phase, now.isoformat())) db.commit() return next_phase except Exception: db.rollback() raise finally: db.close()