275 lines
9.3 KiB
Python
275 lines
9.3 KiB
Python
#!/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=<hook chain exited; broker /proc snapshot above is authoritative>")
|
|
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 <protected-socket> {runtime} <runtime args>"
|
|
)
|
|
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()
|