#!/usr/bin/env python3 """Constrained recovery command: the sole ungated Mosaic mutator. This is deliberately a thin client of the broker's recovery entrypoint. It never accepts receipt text or a caller-provided challenge: the broker mints the fresh challenge, delivers its exact receipt envelope, and later asks the trusted ReceiptObserver seam to observe that same pending cycle. """ from __future__ import annotations import argparse import json import os import socket import sys from collections.abc import Mapping, Sequence from pathlib import Path from typing import Final # The out-of-process recovery command is intentionally runnable with `python # -I`; locate its shipped construction module without caller-controlled paths. _MODULE_DIRECTORY = str(Path(__file__).resolve().parent) if _MODULE_DIRECTORY not in sys.path: sys.path.insert(0, _MODULE_DIRECTORY) from normative_fragments import build_payload_from_wire MAX_FRAME: Final = 64 * 1024 BROKER_TIMEOUT_SECONDS: Final = 1.5 def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]: payload = (json.dumps(request, separators=(",", ":")) + "\n").encode() if len(payload) > MAX_FRAME: raise ValueError("recovery request too large") response = bytearray() with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection: connection.settimeout(BROKER_TIMEOUT_SECONDS) connection.connect(str(socket_path)) connection.sendall(payload) connection.shutdown(socket.SHUT_WR) while len(response) <= MAX_FRAME: chunk = connection.recv(min(4096, MAX_FRAME + 1 - len(response))) if not chunk: break response.extend(chunk) if len(response) > MAX_FRAME or not response.endswith(b"\n"): raise ValueError("invalid broker reply") value = json.loads(response) if not isinstance(value, dict): raise ValueError("invalid broker reply") return value def load_construction(path: Path) -> dict[str, object]: raw = path.read_bytes() if len(raw) > MAX_FRAME: raise ValueError("construction exceeds broker frame limit") value = json.loads(raw) if not isinstance(value, dict): raise ValueError("construction must be an object") return value def identity(environ: Mapping[str, str]) -> tuple[Path, str, int, str]: socket_path = Path(environ["MOSAIC_LEASE_BROKER_SOCKET"]) session_id = environ["MOSAIC_LEASE_SESSION_ID"] generation = int(environ["MOSAIC_RUNTIME_GENERATION"]) runtime = environ["MOSAIC_LEASE_RUNTIME"] if generation < 0 or runtime not in {"claude", "pi"}: raise ValueError("invalid runtime identity") return socket_path, session_id, generation, runtime def begin( construction_path: Path, compaction_epoch: int, request_epoch: int, environ: Mapping[str, str], ) -> dict[str, object]: if compaction_epoch < 0 or request_epoch < 0: raise ValueError("epochs must be non-negative") construction = load_construction(construction_path) # Invoke the shared WI-5 construction before asking the broker to repeat # its authoritative admission/build. No digest or receipt enters via CLI. built = build_payload_from_wire(construction) if ( built.injectionDecision != "ACCEPTED" or not built.promotion or not isinstance(built.h_source, str) or not isinstance(built.h_payload, str) ): raise ValueError("payload construction refused") socket_path, session_id, generation, runtime = identity(environ) return broker_request(socket_path, { "action": "begin_recovery", "session_id": session_id, "runtime_generation": generation, "runtime": runtime, "binding": { "compaction_epoch": compaction_epoch, "request_epoch": request_epoch, "h_source": built.h_source, "h_payload": built.h_payload, "schema_version": 1, }, "construction": construction, }) def complete(environ: Mapping[str, str]) -> dict[str, object]: socket_path, session_id, generation, _runtime = identity(environ) # No receipt or challenge argument exists: recovery completion can only use # the broker's current recovery cycle and its trusted observer seam. return broker_request(socket_path, { "action": "complete_recovery", "session_id": session_id, "runtime_generation": generation, }) def main(argv: Sequence[str] | None = None, *, environ: Mapping[str, str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) subcommands = parser.add_subparsers(dest="phase", required=True) begin_parser = subcommands.add_parser("begin", help="mint and deliver a fresh recovery receipt") begin_parser.add_argument("--construction", required=True, type=Path) begin_parser.add_argument("--compaction-epoch", required=True, type=int) begin_parser.add_argument("--request-epoch", required=True, type=int) subcommands.add_parser("complete", help="observe and promote only the current recovery receipt") arguments = parser.parse_args(argv) source_environment = os.environ if environ is None else environ try: reply = ( begin( arguments.construction, arguments.compaction_epoch, arguments.request_epoch, source_environment, ) if arguments.phase == "begin" else complete(source_environment) ) except (KeyError, OSError, ValueError, json.JSONDecodeError) as error: print(f"Mosaic constrained recovery refused: {error}", file=sys.stderr) return 2 print(json.dumps(reply, separators=(",", ":"))) return 0 if reply.get("ok") is True else 2 if __name__ == "__main__": raise SystemExit(main())