#!/usr/bin/env python3 """Register a runtime parent with the lease broker, then exec without changing PID.""" from __future__ import annotations import argparse import json import os import socket import sys import time from collections.abc import Callable, Mapping, Sequence from datetime import datetime, timezone from pathlib import Path from typing import Final from activation_version_gate import ( EXPECTED_ACTIVATION_CAPABILITY, ActivationCapability, VersionCouplingError, assert_activation_capability_matches, default_probe_activation_capability, ) from lease_generation import initialize_runtime_generation MAX_FRAME: Final = 64 * 1024 BROKER_TIMEOUT_SECONDS: Final = 1.5 CLAUDE_DANGEROUS_FLAG: Final = "--dangerously-skip-permissions" # Distinct, non-overlapping exit code for the C4 version-coupling gate (see # `activation_version_gate.py`) — deliberately different from the `1` # (broker registration failed closed) and `64` (usage error) codes already # owned by this script, so a version-skew denial is unambiguous in caller # logs/tests and is never confused with a broker-availability failure. EXIT_VERSION_SKEW: Final = 65 def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]: payload = (json.dumps(request, separators=(",", ":")) + "\n").encode() 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 _self_starttime() -> str | None: """Field 22 of our own /proc stat — the anchor starttime the broker records. Read past the comm field's parens, since a process name may contain them. """ try: raw = Path(f"/proc/{os.getpid()}/stat").read_text() return raw.rsplit(")", 1)[1].split()[19] except (OSError, IndexError, ValueError): return None def _append_launch_record(environ: Mapping[str, str], record: dict[str, object]) -> None: """Append one NDJSON event to the #797 Runtime Session Ledger. `fleet/run/sessions/` is operator-classified in framework-manifest.txt and is already covered by test-upgrade-manifest-guard.sh, so an upgrade can neither overwrite nor prune it. Files 0600 under a 0700 dir, matching what that guard asserts. Never raises: a launch must not be denied over bookkeeping. But it also never fails silently — a missing record is exactly the kind of gap that made the 2026-08-06 MUTATOR_UNVERIFIED investigation cost a day. """ try: mosaic_home = environ.get("MOSAIC_HOME") or str(Path.home() / ".config" / "mosaic") directory = Path(mosaic_home) / "fleet" / "run" / "sessions" directory.mkdir(parents=True, exist_ok=True) os.chmod(directory, 0o700) framed = { "seq": time.time_ns() // 1_000_000, "ts": datetime.now(timezone.utc).isoformat(), **record, } path = directory / "events.ndjson" descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) with os.fdopen(descriptor, "w") as handle: handle.write(json.dumps(framed, separators=(",", ":")) + "\n") except (OSError, ValueError, TypeError) as error: print(f"[mosaic] WARNING: launch record not written: {error}", file=sys.stderr) def main( argv: Sequence[str] | None = None, *, environ: Mapping[str, str] | None = None, request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request, execute: Callable[[str, list[str], dict[str, str]], object] = os.execvpe, initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation, probe_activation_capability: Callable[ [Mapping[str, str]], ActivationCapability | None ] = default_probe_activation_capability, expected_activation_capability: ActivationCapability = EXPECTED_ACTIVATION_CAPABILITY, ) -> int: parser = argparse.ArgumentParser() parser.add_argument("--runtime", required=True, choices=("claude", "pi")) parser.add_argument("--dangerous", action="store_true") parser.add_argument("command", nargs=argparse.REMAINDER) arguments = parser.parse_args(argv) command = arguments.command if command and command[0] == "--": command = command[1:] if not command: print("lease-gated runtime command is required", file=sys.stderr) return 64 if arguments.dangerous: if arguments.runtime != "claude" or Path(command[0]).name != "claude": print("dangerous mode is supported only for the Claude runtime", file=sys.stderr) return 64 command = [command[0], CLAUDE_DANGEROUS_FLAG, *command[1:]] source_environment = os.environ if environ is None else environ # C4 version-coupling gate (#869 Point-1): before this ENFORCEMENT half # chains into anything, assert that the ACTIVATION contract it is about # to rely on (MOSAIC_LEASE_* injection, broker chaining) matches what # this enforcement build expects. This is a build/deploy-defect check, # not a broker-availability question, so it runs before — and # independently of — broker registration below, and it FAILS LOUD: a # clear stderr message plus a dedicated non-zero exit code, never a # silent pass and never folded into the generic registration-failure # branch. try: activation_capability = probe_activation_capability(source_environment) assert_activation_capability_matches( activation_capability, expected_activation_capability, ) except VersionCouplingError as version_error: print(str(version_error), file=sys.stderr) return EXIT_VERSION_SKEW try: socket_path = Path(source_environment["MOSAIC_LEASE_BROKER_SOCKET"]) generation = int(source_environment.get("MOSAIC_RUNTIME_GENERATION", "1")) if generation < 0: raise ValueError("invalid generation") reply = request( socket_path, { "action": "register_anchor", "runtime_generation": generation, }, ) session_id = reply.get("session_id") if ( reply.get("ok") is not True or not isinstance(session_id, str) or len(session_id) != 64 or any(character not in "0123456789abcdef" for character in session_id) ): raise ValueError("registration refused") generation_file = socket_path.parent / f"generation-{session_id}.state" initialize_generation(generation_file, generation) except (KeyError, ValueError, OSError, json.JSONDecodeError): print("Mosaic lease broker registration failed; runtime launch denied.", file=sys.stderr) return 1 # Immutable launch record, half two. `mosaic` wrote `session.launch` with the # config/provenance it knows; only this process knows the broker session id # and the activation capability it just asserted. os.execvpe preserves the # PID, so this PID is BOTH the anchor pid and the join key back to that # record. Never fatal — bookkeeping must not deny a launch — but never # silent either. _append_launch_record( source_environment, { "kind": "lease.register", # Joins back to `mosaic`'s session.launch record. NOT pid: execRuntime() # spawns rather than execs, so this process is a CHILD of mosaic with a # different pid. This pid IS the broker anchor pid (os.execvpe below # preserves it), which is a separate and still-useful fact. "launch_id": source_environment.get("MOSAIC_LAUNCH_ID"), "pid": os.getpid(), "runtime": arguments.runtime, "session_id": session_id, "runtime_generation": generation, "generation_file": str(generation_file), "anchor_starttime": _self_starttime(), "activation_capability": activation_capability, "command": Path(command[0]).name, }, ) environment = dict(source_environment) environment["MOSAIC_LEASE_SESSION_ID"] = session_id environment["MOSAIC_RUNTIME_GENERATION"] = str(generation) environment["MOSAIC_LEASE_GENERATION_FILE"] = str(generation_file) environment["MOSAIC_LEASE_RUNTIME"] = arguments.runtime # Matches daemon.py's production default; deployments using a distinct # observer socket may set this authenticated transport path explicitly. environment.setdefault( "MOSAIC_RECEIPT_OBSERVER_SOCKET", str(socket_path.with_name("receipt-observer.sock")), ) try: execute(command[0], command, environment) except OSError: print("Mosaic lease-gated runtime exec failed.", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())