#!/usr/bin/env python3 """Claude Stop hook that completes a pending operator-triggered promotion.""" from __future__ import annotations import fcntl import json import os import secrets import stat import subprocess import sys import time from collections.abc import Callable, Mapping from pathlib import Path from typing import Final, NamedTuple, TextIO MAX_FRAME: Final = 64 * 1024 PROMOTER_TIMEOUT_SECONDS: Final = 10.0 LEASE_TTL_SECONDS: Final = 60 * 60 PROMOTER: Final = Path(__file__).resolve().with_name("lease_promote.py") PENDING_DIRECTORY: Final = "mosaic-lease" LOCK_FILE: Final = "promotion.lock" RESULT_FILE: Final = "last-result.json" TERMINAL_FAILURE_CODES: Final = frozenset( { "RECEIPT_REPLAY", "RECEIPT_MISMATCH", "INVALID_LEASE_TRANSITION", "PROMOTION_TOKEN_INVALID", } ) class PendingChallenge(NamedTuple): value: str device: int inode: int def reject_duplicate_json_keys(pairs: list[tuple[str, object]]) -> dict[str, object]: value: dict[str, object] = {} for key, item in pairs: if key in value: raise ValueError("duplicate promoter JSON key") value[key] = item return value def session_pending_name(environ: Mapping[str, str]) -> tuple[Path, str]: runtime_dir = Path(environ["XDG_RUNTIME_DIR"]) session_id = environ["MOSAIC_LEASE_SESSION_ID"] if not runtime_dir.is_absolute(): raise ValueError("XDG_RUNTIME_DIR must be absolute") if len(session_id) != 64 or any(character not in "0123456789abcdef" for character in session_id): raise ValueError("invalid lease session id") return runtime_dir, f"pending-{session_id}" def open_pending_directory(runtime_dir: Path) -> int | None: directory_flags = ( os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0) ) try: runtime_descriptor = os.open(runtime_dir, directory_flags) except FileNotFoundError: return None try: runtime_metadata = os.fstat(runtime_descriptor) if ( not stat.S_ISDIR(runtime_metadata.st_mode) or runtime_metadata.st_uid != os.getuid() or stat.S_IMODE(runtime_metadata.st_mode) != 0o700 ): raise ValueError("unsafe XDG runtime directory") try: descriptor = os.open(PENDING_DIRECTORY, directory_flags, dir_fd=runtime_descriptor) except FileNotFoundError: return None finally: os.close(runtime_descriptor) metadata = os.fstat(descriptor) if ( not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o700 ): os.close(descriptor) raise ValueError("unsafe promotion pending directory") return descriptor def acquire_lock(directory_descriptor: int) -> int: flags = ( os.O_RDWR | os.O_CREAT | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) ) descriptor = os.open(LOCK_FILE, flags, 0o600, dir_fd=directory_descriptor) metadata = os.fstat(descriptor) if ( not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o600 ): os.close(descriptor) raise ValueError("unsafe promotion lock file") try: fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: os.close(descriptor) raise return descriptor def read_pending(directory_descriptor: int, name: str) -> PendingChallenge | None: flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) try: descriptor = os.open(name, flags, dir_fd=directory_descriptor) except FileNotFoundError: return None try: metadata = os.fstat(descriptor) if ( not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o600 or metadata.st_size <= 0 or metadata.st_size > MAX_FRAME ): raise ValueError("unsafe promotion pending file") raw = os.read(descriptor, MAX_FRAME + 1) finally: os.close(descriptor) if len(raw) > MAX_FRAME: raise ValueError("oversized promotion challenge") challenge = raw.decode("utf-8") if ( len(challenge) != 64 or any(character not in "0123456789abcdef" for character in challenge) ): raise ValueError("invalid promotion challenge") return PendingChallenge(challenge, metadata.st_dev, metadata.st_ino) def write_result( directory_descriptor: int, attempt_id: str, verified: bool, reason: str | None, session_id: str, wall_clock: float, ) -> None: result = { "attempt_id": attempt_id, "expires_at_wallclock": wall_clock + LEASE_TTL_SECONDS if verified else None, "reason": reason, "session_id": session_id, "ts": wall_clock, "verified": verified, } temporary = f".{RESULT_FILE}.tmp-{secrets.token_hex(8)}" flags = ( os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) ) descriptor = os.open(temporary, flags, 0o600, dir_fd=directory_descriptor) try: os.fchmod(descriptor, 0o600) with os.fdopen(descriptor, "w", encoding="utf-8", closefd=False) as stream: json.dump(result, stream, separators=(",", ":"), sort_keys=True) stream.flush() os.fsync(stream.fileno()) os.replace( temporary, RESULT_FILE, src_dir_fd=directory_descriptor, dst_dir_fd=directory_descriptor, ) os.fsync(directory_descriptor) except Exception: try: os.unlink(temporary, dir_fd=directory_descriptor) except FileNotFoundError: pass raise finally: os.close(descriptor) def delete_pending_if_unchanged( directory_descriptor: int, name: str, pending: PendingChallenge, error_stream: TextIO, ) -> None: quarantine = f".{name}.delete-{secrets.token_hex(8)}" try: os.rename( name, quarantine, src_dir_fd=directory_descriptor, dst_dir_fd=directory_descriptor, ) except FileNotFoundError: return except OSError as error: print(f"Mosaic promotion could not quarantine pending file: {error}", file=error_stream) return try: moved = os.stat( quarantine, dir_fd=directory_descriptor, follow_symlinks=False, ) if (moved.st_dev, moved.st_ino) == (pending.device, pending.inode): os.unlink(quarantine, dir_fd=directory_descriptor) os.fsync(directory_descriptor) return print("Mosaic promotion pending file changed; preserving replacement.", file=error_stream) try: os.link( quarantine, name, src_dir_fd=directory_descriptor, dst_dir_fd=directory_descriptor, follow_symlinks=False, ) except FileExistsError: print( f"Mosaic promotion preserved replacement as {quarantine}.", file=error_stream, ) else: os.unlink(quarantine, dir_fd=directory_descriptor) os.fsync(directory_descriptor) except OSError as error: print(f"Mosaic promotion could not resolve pending file: {error}", file=error_stream) def parse_reply(completed: subprocess.CompletedProcess[str]) -> dict[str, object] | None: if completed.returncode != 0: return None try: value = json.loads( completed.stdout, object_pairs_hook=reject_duplicate_json_keys, ) except (json.JSONDecodeError, RecursionError, TypeError, ValueError): return None if not isinstance(value, dict): return None if set(value) == {"stage", "ok", "state"}: if ( value.get("stage") == "promote_lease" and value.get("ok") is True and value.get("state") == "VERIFIED" ): return value return None if set(value) == {"stage", "ok", "code"}: if ( value.get("stage") in {"observe_receipt", "promote_lease"} and value.get("ok") is False and isinstance(value.get("code"), str) and value.get("code") ): return value return None def main( *, environ: Mapping[str, str] | None = None, stderr: TextIO | None = None, run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run, now: Callable[[], float] = time.time, ) -> int: source_environment = os.environ if environ is None else environ error_stream = sys.stderr if stderr is None else stderr directory_descriptor: int | None = None lock_descriptor: int | None = None try: runtime_dir, pending_name = session_pending_name(source_environment) session_id = source_environment["MOSAIC_LEASE_SESSION_ID"] directory_descriptor = open_pending_directory(runtime_dir) if directory_descriptor is None: return 0 try: lock_descriptor = acquire_lock(directory_descriptor) except (BlockingIOError, FileNotFoundError): print("Mosaic promotion completion deferred: promotion is in progress.", file=error_stream) return 0 pending = read_pending(directory_descriptor, pending_name) if pending is None: return 0 completed = run( [ sys.executable, "-I", "-S", "-B", str(PROMOTER), "--complete", pending.value, ], check=False, capture_output=True, text=True, env=dict(source_environment), timeout=PROMOTER_TIMEOUT_SECONDS, ) reply = parse_reply(completed) if reply is not None and reply.get("ok") is True: write_result(directory_descriptor, pending.value, True, None, session_id, now()) delete_pending_if_unchanged( directory_descriptor, pending_name, pending, error_stream, ) print("Mosaic lease promotion completed.", file=error_stream) return 0 if reply is not None: code = str(reply["code"]) print(f"Mosaic promotion incomplete: {code}.", file=error_stream) if code in TERMINAL_FAILURE_CODES: write_result(directory_descriptor, pending.value, False, code, session_id, now()) delete_pending_if_unchanged( directory_descriptor, pending_name, pending, error_stream, ) else: diagnostic = completed.stderr.strip() or f"promoter exit {completed.returncode}" print(f"Mosaic promotion retryable failure: {diagnostic}.", file=error_stream) except (KeyError, OSError, RecursionError, UnicodeError, ValueError, subprocess.SubprocessError) as error: print(f"Mosaic promotion completion deferred: {type(error).__name__}: {error}", file=error_stream) finally: if lock_descriptor is not None: os.close(lock_descriptor) if directory_descriptor is not None: os.close(directory_descriptor) return 0 if __name__ == "__main__": raise SystemExit(main())