#!/usr/bin/env python3 """Run P1 against the real installed Mosaic→Pi and Mosaic→Claude chains.""" from __future__ import annotations import argparse import json import os import shutil import signal import socket import subprocess import sys import tempfile import time from pathlib import Path from typing import Any HERE = Path(__file__).resolve().parent def wait_for(predicate, description: str, timeout: float = 30.0) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: if predicate(): return time.sleep(0.05) raise TimeoutError(f"timed out waiting for {description}") def read_records(path: Path) -> list[dict[str, Any]]: if not path.exists(): return [] return [json.loads(line) for line in path.read_text().splitlines() if line] def socket_request(path: Path, payload: dict[str, object]) -> dict[str, object]: conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) conn.connect(str(path)) conn.sendall((json.dumps(payload) + "\n").encode()) response = json.loads(conn.makefile("r", encoding="utf-8").readline()) conn.close() return response def start_broker(root: Path) -> tuple[subprocess.Popen[str], Path, Path, Path]: socket_path = root / "broker.sock" log_path = root / "broker.jsonl" state_path = root / "state.json" broker = subprocess.Popen( [ sys.executable, str(HERE / "p1_broker.py"), "--socket", str(socket_path), "--log", str(log_path), "--state", str(state_path), ], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) wait_for(socket_path.exists, "broker socket") return broker, socket_path, log_path, state_path def print_ps(record: dict[str, Any]) -> None: chain = record.get("ancestry", []) pids = [str(node["pid"]) for node in chain if Path(f"/proc/{node['pid']}").exists()] if not pids: print("ps_snapshot=") return command = [ "ps", "-o", "pid=,ppid=,lstart=,uid=,gid=,comm=", "-p", ",".join(pids), ] print("$ " + " ".join(command)) print(subprocess.check_output(command, text=True).rstrip()) def run_runtime(runtime: str) -> None: with tempfile.TemporaryDirectory(prefix=f"gate0-p1-{runtime}-") as temp: root = Path(temp) workspace = root / "workspace" workspace.mkdir() broker, socket_path, log_path, state_path = start_broker(root) env = os.environ.copy() env.update( { "GATE0_BROKER_SOCKET": str(socket_path), "MOSAIC_PI_FORCE_SKILLS": "", "PI_SKIP_VERSION_CHECK": "1", } ) stdout_path = root / f"{runtime}.stdout" stderr_path = root / f"{runtime}.stderr" if runtime == "pi": runtime_args = [ "--mode", "rpc", "--no-session", "--no-extensions", "--no-context-files", "--no-prompt-templates", "--extension", str(HERE / "p1_pi_extension.ts"), ] else: settings = root / "claude-settings.json" settings.write_text( json.dumps( { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": f'python3 "{HERE / "p1_hook_client.py"}"', "timeout": 20, } ] } ] } } ) ) runtime_args = [ "--settings", str(settings), "--model", "haiku", "--print", "--output-format", "stream-json", "--verbose", "--include-hook-events", "--max-budget-usd", "0.03", "Reply exactly: OK", ] out = stdout_path.open("w", encoding="utf-8") err = stderr_path.open("w", encoding="utf-8") anchor = subprocess.Popen( [ sys.executable, str(HERE / "p1_anchor_exec.py"), "--socket", str(socket_path), runtime, *runtime_args, ], cwd=workspace, env=env, stdin=subprocess.PIPE if runtime == "pi" else subprocess.DEVNULL, stdout=out, stderr=err, text=True, start_new_session=True, ) try: wait_for(state_path.exists, "anchor registration") attacker = subprocess.run( [ sys.executable, str(HERE / "p1_sibling_attacker.py"), "--socket", str(socket_path), "--state", str(state_path), ], text=True, capture_output=True, check=False, ) if runtime == "pi": assert anchor.stdin is not None anchor.stdin.write('{"id":"state","type":"get_state"}\n') anchor.stdin.flush() wait_for( lambda: any(r.get("event") == "resolve-hook" for r in read_records(log_path)), f"{runtime} supported hook/extension broker contact", timeout=60, ) if runtime == "claude": try: anchor.wait(timeout=90) except subprocess.TimeoutExpired: pass records = read_records(log_path) state = json.loads(state_path.read_text()) resolve = next(r for r in records if r.get("event") == "resolve-hook") reject = next(r for r in records if r.get("event") == "claim-session") if resolve.get("decision") != "ACCEPT": raise AssertionError(f"{runtime} hook ancestry was not accepted: {resolve}") if reject.get("decision") != "REJECT": raise AssertionError(f"{runtime} sibling substitution was not rejected: {reject}") if attacker.returncode != 0: raise AssertionError(f"{runtime} sibling probe did not observe rejection: {attacker.stderr}") print(f"=== P1 {runtime.upper()} REAL LAUNCH ===") print("machine_assertions=PASS") print( "$ python3 docs/compaction-refresh/probes/p1_anchor_exec.py " f"--socket {runtime} " ) print("registered_anchor=" + json.dumps(state["anchor"], sort_keys=True)) print("broker_minted_session_id=" + state["session_id"]) print("hook_or_extension_record=" + json.dumps(resolve, sort_keys=True)) print("sibling_attack_record=" + json.dumps(reject, sort_keys=True)) print("sibling_process_stdout=" + attacker.stdout.strip()) print(f"sibling_process_exit={attacker.returncode}") print_ps(resolve) print("launcher_stderr_excerpt:") for line in stderr_path.read_text(errors="replace").splitlines()[:12]: print(" " + line[:500]) runtime_lines = stdout_path.read_text(errors="replace").splitlines() print("runtime_stdout_excerpt:") for line in runtime_lines[:8]: print(" " + line[:500]) hook_lines = [ line for line in runtime_lines if "hook" in line.lower() or "GATE0_P1_SUPPORTED_HOOK" in line ] print("runtime_hook_event_excerpt:") for line in hook_lines[:8]: print(" " + line[:1000]) print() finally: if anchor.poll() is None: try: os.killpg(anchor.pid, signal.SIGTERM) except ProcessLookupError: pass try: anchor.wait(timeout=5) except subprocess.TimeoutExpired: os.killpg(anchor.pid, signal.SIGKILL) anchor.wait(timeout=5) out.close() err.close() try: socket_request(socket_path, {"action": "shutdown"}) except OSError: pass try: broker.wait(timeout=5) except subprocess.TimeoutExpired: broker.kill() broker.wait() def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--runtime", choices=["pi", "claude", "both"], default="both") ns = parser.parse_args() if ns.runtime in {"pi", "both"}: run_runtime("pi") if ns.runtime in {"claude", "both"}: run_runtime("claude") if __name__ == "__main__": main()