test(827): capture Gate0 runtime evidence

This commit is contained in:
ms-lead-reviewer
2026-07-17 19:37:56 -05:00
parent d801d6c4c8
commit d5c599e2b0
25 changed files with 2587 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Register this PID as anchor, then exec the real `mosaic yolo` launcher."""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
def request(socket_path: str, payload: dict[str, object]) -> dict[str, object]:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(socket_path)
conn.sendall((json.dumps(payload) + "\n").encode())
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
return response
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("runtime", choices=["pi", "claude"])
parser.add_argument("args", nargs=argparse.REMAINDER)
ns = parser.parse_args()
response = request(ns.socket, {"action": "register-anchor", "runtime": ns.runtime})
if response.get("decision") != "ACCEPT":
raise SystemExit("anchor registration refused")
os.environ["GATE0_SESSION_ID"] = str(response["session_id"])
argv = ["mosaic", "yolo", ns.runtime, *ns.args]
print(
json.dumps(
{
"event": "anchor-exec",
"pid": os.getpid(),
"argv": ["mosaic", "yolo", ns.runtime, f"<{len(ns.args)} runtime args>"],
"note": "os.execvpe retains pid and /proc starttime",
},
sort_keys=True,
),
file=sys.stderr,
flush=True,
)
os.execvpe("mosaic", argv, os.environ)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Gate0 P1 broker prototype: peercred anchor minting and /proc ancestry checks."""
from __future__ import annotations
import argparse
import json
import os
import secrets
import socket
import stat
import struct
import sys
from pathlib import Path
from typing import Any
def proc_node(pid: int) -> dict[str, Any]:
text = Path(f"/proc/{pid}/stat").read_text()
close = text.rfind(")")
comm = text[text.find("(") + 1 : close]
fields = text[close + 2 :].split()
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\0")
return {
"pid": pid,
"ppid": int(fields[1]),
"starttime_ticks": int(fields[19]),
"comm": comm,
"exe": os.readlink(f"/proc/{pid}/exe"),
"argv0": cmdline[0].decode(errors="replace") if cmdline and cmdline[0] else "",
"argc": len([part for part in cmdline if part]),
}
def ancestry(peer_pid: int, anchor: dict[str, Any] | None) -> tuple[list[dict[str, Any]], bool, str]:
chain: list[dict[str, Any]] = []
pid = peer_pid
seen: set[int] = set()
try:
while pid > 0 and pid not in seen:
seen.add(pid)
node = proc_node(pid)
chain.append(node)
if anchor and pid == anchor["pid"]:
if node["starttime_ticks"] != anchor["starttime_ticks"]:
return chain, False, "anchor-starttime-mismatch"
break
pid = node["ppid"]
else:
return chain, False, "anchor-not-reached"
if not anchor or chain[-1]["pid"] != anchor["pid"]:
return chain, False, "anchor-not-reached"
# Re-read every node after the walk. A disappearing PID or changed
# starttime invalidates the complete chain (PID-reuse/race closure).
for original in chain:
again = proc_node(original["pid"])
if again["starttime_ticks"] != original["starttime_ticks"]:
return chain, False, f"starttime-race:{original['pid']}"
return chain, True, "ancestry-reaches-registered-anchor"
except (FileNotFoundError, ProcessLookupError, PermissionError) as exc:
return chain, False, f"proc-walk-failed:{type(exc).__name__}"
def emit(log_file: Path, record: dict[str, Any]) -> None:
line = json.dumps(record, sort_keys=True)
with log_file.open("a", encoding="utf-8") as out:
out.write(line + "\n")
print(line, flush=True)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--log", required=True)
parser.add_argument("--state", required=True)
args = parser.parse_args()
socket_path = Path(args.socket)
log_file = Path(args.log)
state_file = Path(args.state)
socket_path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(socket_path.parent, 0o700)
socket_path.unlink(missing_ok=True)
log_file.unlink(missing_ok=True)
state_file.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(8)
anchor: dict[str, Any] | None = None
session_id: str | None = None
emit(
log_file,
{
"event": "broker-listen",
"pid": os.getpid(),
"socket": str(socket_path),
"directory_mode": f"{stat.S_IMODE(socket_path.parent.stat().st_mode):04o}",
"socket_mode": f"{stat.S_IMODE(socket_path.stat().st_mode):04o}",
},
)
while True:
conn, _ = server.accept()
with conn:
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
peer_pid, peer_uid, peer_gid = struct.unpack("3i", raw)
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
action = request.get("action")
if action == "register-anchor" and anchor is None:
anchor = proc_node(peer_pid)
session_id = secrets.token_hex(16)
state = {"session_id": session_id, "anchor": anchor}
state_file.write_text(json.dumps(state, sort_keys=True) + "\n")
record = {
"event": "anchor-minted",
"decision": "ACCEPT",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
"anchor": anchor,
"session_id": session_id,
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
continue
if action in {"resolve-hook", "claim-session"}:
chain, reaches, reason = ancestry(peer_pid, anchor)
claimed = request.get("session_id")
claim_ok = action == "resolve-hook" or claimed == session_id
accepted = bool(anchor and session_id and reaches and claim_ok)
if action == "claim-session" and claimed != session_id:
reason = "unknown-session-id"
elif action == "claim-session" and claimed == session_id and not reaches:
reason = "victim-id-known-but-ancestry-mismatch"
record = {
"event": action,
"decision": "ACCEPT" if accepted else "REJECT",
"reason": reason,
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
"claimed_session_id": claimed,
"resolved_session_id": session_id if accepted else None,
"anchor": anchor,
"ancestry": chain,
"starttimes_rechecked": reaches,
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
continue
if action == "shutdown":
record = {
"event": "broker-shutdown",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
break
record = {
"event": "invalid-request",
"decision": "REJECT",
"peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid},
}
emit(log_file, record)
conn.sendall((json.dumps(record) + "\n").encode())
server.close()
socket_path.unlink(missing_ok=True)
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"P1 broker fatal: {type(exc).__name__}: {exc}", file=sys.stderr)
raise

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Claude SessionStart hook client for P1 ancestry evidence."""
from __future__ import annotations
import json
import os
import socket
import sys
def main() -> None:
# Consume the real Claude hook payload without recording transcript paths or
# prompt content in the evidence artifact.
hook_input = json.load(sys.stdin)
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(os.environ["GATE0_BROKER_SOCKET"])
conn.sendall((json.dumps({"action": "resolve-hook"}) + "\n").encode())
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
event_name = hook_input.get("hook_event_name")
if response.get("decision") != "ACCEPT":
print(f"Gate0 broker rejected {event_name} hook ancestry", file=sys.stderr)
raise SystemExit(2)
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": event_name,
"additionalContext": "GATE0_P1_SUPPORTED_HOOK_ANCESTRY_ACCEPTED",
}
}
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,35 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import net from 'node:net';
async function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
const socketPath = process.env['GATE0_BROKER_SOCKET'];
if (!socketPath) throw new Error('GATE0_BROKER_SOCKET missing');
return await new Promise((resolve, reject) => {
const socket = net.createConnection(socketPath);
let buffer = '';
socket.setEncoding('utf8');
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
socket.on('data', (chunk) => {
buffer += chunk;
const newline = buffer.indexOf('\n');
if (newline < 0) return;
socket.end();
resolve(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
});
socket.on('error', reject);
});
}
export default function register(pi: ExtensionAPI) {
pi.on('session_start', async () => {
const response = await brokerRequest({ action: 'resolve-hook', runtime: 'pi-extension' });
if (response['decision'] !== 'ACCEPT') {
throw new Error(`P1 broker rejected Pi extension ancestry: ${response['reason']}`);
}
});
pi.registerCommand('gate0-p1-ready', {
description: 'Return only after the P1 session_start ancestry hook completed',
handler: async () => undefined,
});
}

View File

@@ -0,0 +1,274 @@
#!/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()

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Same-UID sibling that attempts to claim the anchor's broker-minted id."""
from __future__ import annotations
import argparse
import json
import os
import socket
import time
from pathlib import Path
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--state", required=True)
ns = parser.parse_args()
state_path = Path(ns.state)
for _ in range(200):
if state_path.exists():
break
time.sleep(0.025)
state = json.loads(state_path.read_text())
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(ns.socket)
conn.sendall(
(
json.dumps(
{"action": "claim-session", "session_id": state["session_id"]},
sort_keys=True,
)
+ "\n"
).encode()
)
response = json.loads(conn.makefile("r", encoding="utf-8").readline())
conn.close()
print(
json.dumps(
{
"attacker_pid": os.getpid(),
"attacker_uid": os.getuid(),
"victim_session_id_known": True,
"broker_decision": response.get("decision"),
"broker_reason": response.get("reason"),
},
sort_keys=True,
)
)
raise SystemExit(0 if response.get("decision") == "REJECT" else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""Force a real Pi HTTP provider response to prove response-hook timing."""
from __future__ import annotations
import json
import os
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from pi_gate0_run import PiRpc, jsonl
HERE = Path(__file__).resolve().parent
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, _format: str, *_args: object) -> None:
return
def do_POST(self) -> None: # noqa: N802
length = int(self.headers.get("content-length", "0"))
self.rfile.read(length)
chunks = [
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}],
},
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [
{"index": 0, "delta": {"content": "TIMING_OK"}, "finish_reason": None}
],
},
{
"id": "gate0-response",
"object": "chat.completion.chunk",
"created": 1,
"model": "gate0-model",
"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
},
]
body = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + "data: [DONE]\n\n"
encoded = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(encoded)))
self.send_header("X-Gate0-Response", "headers-before-stream")
self.end_headers()
self.wfile.write(encoded)
self.wfile.flush()
def main() -> None:
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
with tempfile.TemporaryDirectory(prefix="gate0-p2-timing-") as temp:
root = Path(temp)
workspace = root / "workspace"
workspace.mkdir()
log = root / "hooks.jsonl"
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(log),
"GATE0_LOCAL_PROVIDER_URL": f"http://127.0.0.1:{port}/v1",
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--no-session",
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--provider",
"gate0-local",
"--model",
"gate0-model",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.prompt_and_settle("timing", "Reply with TIMING_OK")
records = jsonl(log)
selected = [
record
for record in records
if record["event"] in {"before_provider_request", "after_provider_response", "message_end"}
and (record["event"] != "message_end" or record.get("role") == "assistant")
]
print("$ python3 docs/compaction-refresh/probes/p2_provider_timing_run.py")
print(f"local_http_endpoint=http://127.0.0.1:{port}/v1/chat/completions")
for record in selected:
print(json.dumps(record, sort_keys=True))
after = next(record for record in selected if record["event"] == "after_provider_response")
message = next(record for record in selected if record["event"] == "message_end")
if not (
after["seq"] < message["seq"]
and after["assistantContentAvailableAtThisHook"] is False
and message["assistantContentObserved"] is True
):
raise AssertionError("provider response/content observation ordering failed")
print("machine_assertions=PASS")
print(f"after_provider_response_seq={after['seq']}")
print(f"message_end_seq={message['seq']}")
print(f"headers_hook_precedes_completed_message={after['seq'] < message['seq']}")
finally:
pi.close()
server.shutdown()
server.server_close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""P3 broker prototype: peercred-keyed runtime_generation and lease revocation."""
from __future__ import annotations
import argparse
import json
import os
import socket
import struct
from pathlib import Path
from typing import Any
def proc_starttime(pid: int) -> int:
text = Path(f"/proc/{pid}/stat").read_text()
close = text.rfind(")")
return int(text[close + 2 :].split()[19])
def emit(log: Path, value: dict[str, Any]) -> None:
with log.open("a", encoding="utf-8") as out:
out.write(json.dumps(value, sort_keys=True) + "\n")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True)
parser.add_argument("--log", required=True)
ns = parser.parse_args()
socket_path = Path(ns.socket)
log_path = Path(ns.log)
socket_path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(socket_path.parent, 0o700)
socket_path.unlink(missing_ok=True)
log_path.unlink(missing_ok=True)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(8)
generations: dict[tuple[int, int], int] = {}
lease_state: dict[tuple[int, int], str] = {}
emit(log_path, {"event": "listen", "pid": os.getpid(), "socket": str(socket_path)})
while True:
conn, _ = server.accept()
with conn:
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
pid, uid, gid = struct.unpack("3i", raw)
starttime = proc_starttime(pid)
request = json.loads(conn.makefile("r", encoding="utf-8").readline())
if request.get("action") == "shutdown-broker":
conn.sendall(b'{"ok":true}\n')
break
identity = (pid, starttime)
if request.get("action") == "promote-probe":
generation = generations.get(identity, 0)
lease_state[identity] = "VERIFIED"
record = {
"event": "probe_lease_promoted",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"generation": generation,
"new_lease_state": "VERIFIED",
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
continue
if request.get("action") == "source-invalid":
old_lease = lease_state.get(identity, "NONE")
lease_state[identity] = "REVOKED"
record = {
"event": "source_invalidation_revoke",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"generation": generations.get(identity, 0),
"source_reason": request.get("reason"),
"prior_lease": old_lease,
"new_lease_state": "REVOKED",
"promotion": False,
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
continue
if request.get("action") != "lifecycle":
conn.sendall(b'{"ok":false,"reason":"invalid-action"}\n')
continue
old_generation = generations.get(identity, 0)
old_lease = lease_state.get(identity, "NONE")
new_generation = old_generation + 1
generations[identity] = new_generation
# Every lifecycle boundary revokes first. A start establishes a new
# UNVERIFIED incarnation; it never inherits prior VERIFIED state.
lease_state[identity] = "UNVERIFIED" if request.get("phase") == "start" else "REVOKED"
record = {
"event": "runtime_generation_bump",
"peercred": {"pid": pid, "uid": uid, "gid": gid},
"starttime_ticks": starttime,
"phase": request.get("phase"),
"reason": request.get("reason"),
"old_generation": old_generation,
"new_generation": new_generation,
"prior_lease": old_lease,
"prior_lease_revoked": True,
"new_lease_state": lease_state[identity],
}
emit(log_path, record)
conn.sendall((json.dumps(record, sort_keys=True) + "\n").encode())
server.close()
socket_path.unlink(missing_ok=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Gate0 P4: exercise Linux SO_PEERCRED and correlate it to /proc."""
from __future__ import annotations
import json
import os
import socket
import stat
import tempfile
from pathlib import Path
def proc_identity(pid: int) -> dict[str, int | str]:
stat_text = Path(f"/proc/{pid}/stat").read_text()
close = stat_text.rfind(")")
fields = stat_text[close + 2 :].split()
# fields[0] is field 3 (state); ppid is field 4 and starttime is field 22.
return {
"pid": pid,
"ppid": int(fields[1]),
"starttime_ticks": int(fields[19]),
"uid": int(Path(f"/proc/{pid}/status").read_text().split("Uid:", 1)[1].split()[0]),
"exe": os.readlink(f"/proc/{pid}/exe"),
}
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-p4-") as tmp:
root = Path(tmp)
os.chmod(root, 0o700)
socket_path = root / "broker.sock"
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
server.listen(1)
child = os.fork()
if child == 0:
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect(str(socket_path))
identity = proc_identity(os.getpid())
client.sendall((json.dumps(identity, sort_keys=True) + "\n").encode())
# Keep /proc/<pid> alive until the server has correlated peercred.
if client.recv(2) != b"OK":
os._exit(2)
client.close()
os._exit(0)
conn, _ = server.accept()
raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
peer_pid = int.from_bytes(raw[0:4], byteorder="little", signed=True)
peer_uid = int.from_bytes(raw[4:8], byteorder="little", signed=True)
peer_gid = int.from_bytes(raw[8:12], byteorder="little", signed=True)
claimed = json.loads(conn.makefile("r", encoding="utf-8").readline())
observed = proc_identity(peer_pid)
conn.sendall(b"OK")
_, status = os.waitpid(child, 0)
root_mode = stat.S_IMODE(root.stat().st_mode)
socket_mode = stat.S_IMODE(socket_path.stat().st_mode)
if not (
peer_pid == claimed["pid"] == observed["pid"]
and peer_uid == claimed["uid"] == observed["uid"]
and claimed["starttime_ticks"] == observed["starttime_ticks"]
and root_mode == 0o700
and socket_mode == 0o600
and os.waitstatus_to_exitcode(status) == 0
):
raise AssertionError("SO_PEERCRED, /proc identity, or socket-mode correlation failed")
print("machine_assertions=PASS")
print(f"server_pid={os.getpid()} server_uid={os.getuid()} server_gid={os.getgid()}")
print(f"socket_path={socket_path}")
print(f"directory_mode={root_mode:04o} socket_mode={socket_mode:04o}")
print(f"SO_PEERCRED pid={peer_pid} uid={peer_uid} gid={peer_gid}")
print("client_claim=" + json.dumps(claimed, sort_keys=True))
print("proc_observed=" + json.dumps(observed, sort_keys=True))
print(f"pid_match={peer_pid == claimed['pid'] == observed['pid']}")
print(f"uid_match={peer_uid == claimed['uid'] == observed['uid']}")
print(
"starttime_match="
+ str(claimed["starttime_ticks"] == observed["starttime_ticks"])
)
print(f"client_exit_status={os.waitstatus_to_exitcode(status)}")
print("same_principal_socket=true")
print(
"posture=0700 parent + 0600 socket excludes other UIDs, but does not prevent "
"the same UID from unlinking/rebinding; distinct-principal system service remains "
"required for a claim stronger than T-C against same-UID counterfeit replacement"
)
conn.close()
server.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Claude SessionStart additionalContext producer for P6 observation."""
from __future__ import annotations
import hashlib
import json
import os
import sys
from pathlib import Path
BLOCK = "\n".join(
[
"GATE0_CLAUDE_ATOMIC_BEGIN",
"segment-01=alpha-2d11",
"segment-02=middle-8e22",
"segment-03=omega-4f33",
"GATE0_CLAUDE_ATOMIC_END",
]
)
def starttime(pid: int) -> int:
text = Path(f"/proc/{pid}/stat").read_text()
return int(text[text.rfind(")") + 2 :].split()[19])
def main() -> None:
hook_input = json.load(sys.stdin)
log = Path(os.environ["GATE0_CLAUDE_HOOK_LOG"])
record = {
"hook_event_name": hook_input.get("hook_event_name"),
"pid": os.getpid(),
"ppid": os.getppid(),
"starttime_ticks": starttime(os.getpid()),
"block_length": len(BLOCK.encode()),
"block_sha256": hashlib.sha256(BLOCK.encode()).hexdigest(),
"emission": "one hookSpecificOutput.additionalContext string field",
}
log.write_text(json.dumps(record, sort_keys=True) + "\n")
print(
json.dumps(
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": BLOCK,
}
}
)
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Run real Claude 2.1.x through `mosaic yolo` for P6 observation."""
from __future__ import annotations
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
BLOCK = "\n".join(
[
"GATE0_CLAUDE_ATOMIC_BEGIN",
"segment-01=alpha-2d11",
"segment-02=middle-8e22",
"segment-03=omega-4f33",
"GATE0_CLAUDE_ATOMIC_END",
]
)
def strings(value: Any):
if isinstance(value, str):
yield value
elif isinstance(value, list):
for item in value:
yield from strings(item)
elif isinstance(value, dict):
for item in value.values():
yield from strings(item)
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-p6-claude-") as temp:
root = Path(temp)
workspace = root / "workspace"
workspace.mkdir()
settings = root / "settings.json"
hook_log = root / "hook.jsonl"
settings.write_text(
json.dumps(
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": f'python3 "{HERE / "p6_claude_hook.py"}"',
"timeout": 20,
}
]
}
]
}
}
)
)
env = os.environ.copy()
env["GATE0_CLAUDE_HOOK_LOG"] = str(hook_log)
command = [
"mosaic",
"yolo",
"claude",
"--settings",
str(settings),
"--model",
"haiku",
"--print",
"--output-format",
"stream-json",
"--verbose",
"--include-hook-events",
"--max-budget-usd",
"0.10",
"Return only the exact full GATE0_CLAUDE_ATOMIC_BEGIN through GATE0_CLAUDE_ATOMIC_END block injected by SessionStart, with no code fence or commentary.",
]
result = subprocess.run(
command,
cwd=workspace,
env=env,
stdin=subprocess.DEVNULL,
text=True,
capture_output=True,
timeout=150,
check=False,
)
events: list[dict[str, Any]] = []
for line in result.stdout.splitlines():
try:
events.append(json.loads(line))
except json.JSONDecodeError:
continue
hook_events = [
event
for event in events
if event.get("type") == "system"
and event.get("subtype") in {"hook_started", "hook_response"}
]
full_matches = [text for event in events for text in strings(event) if BLOCK in text]
exact_matches = [text for event in events for text in strings(event) if text == BLOCK]
assistant_texts: list[str] = []
for event in events:
if event.get("type") != "assistant":
continue
for text in strings(event.get("message", {})):
if "GATE0_CLAUDE_ATOMIC_BEGIN" in text:
assistant_texts.append(text)
if result.returncode != 0:
raise AssertionError(f"Claude probe exited {result.returncode}")
if not any(event.get("subtype") == "hook_response" and event.get("outcome") == "success" for event in hook_events):
raise AssertionError("Claude SessionStart hook did not complete successfully")
if BLOCK not in exact_matches:
raise AssertionError("Claude did not return an exact full-block field")
print("$ python3 docs/compaction-refresh/probes/p6_claude_run.py")
print("machine_assertions=PASS")
print("command=mosaic yolo claude --settings <isolated> --model haiku --print --output-format stream-json --verbose --include-hook-events <prompt>")
print("claude_version=" + subprocess.check_output(["claude", "--version"], text=True).strip())
print("mosaic_version=" + subprocess.check_output(["mosaic", "--version"], text=True).strip())
print(f"exit_code={result.returncode}")
print("hook_process_log=" + hook_log.read_text().strip())
for event in hook_events:
print("hook_stream_event=" + json.dumps(event, sort_keys=True))
print(f"block_length={len(BLOCK.encode())}")
print(f"block_sha256={hashlib.sha256(BLOCK.encode()).hexdigest()}")
print(f"stream_fields_containing_full_block={len(full_matches)}")
print(f"stream_fields_exactly_equal_block={len(exact_matches)}")
for text in assistant_texts:
print(f"assistant_copy_length={len(text.encode())}")
print(f"assistant_copy_sha256={hashlib.sha256(text.encode()).hexdigest()}")
print(f"assistant_copy_exact={text == BLOCK}")
print("assistant_copy=" + json.dumps(text))
if result.stderr.strip():
print("stderr_excerpt=" + json.dumps(result.stderr.splitlines()[:10]))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,350 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { Type } from 'typebox';
import { createHash, randomUUID } from 'node:crypto';
import { readFileSync, appendFileSync, statSync } from 'node:fs';
import net from 'node:net';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
const SELF = resolve(fileURLToPath(import.meta.url).split('?')[0]!);
const LOG = process.env['GATE0_PI_LOG'];
const CONTEXT_BLOCK =
process.env['GATE0_PI_CONTEXT_BLOCK'] ??
[
'GATE0_PI_ATOMIC_BEGIN',
'segment-01=alpha-7e31',
'segment-02=middle-9c42',
'segment-03=omega-5b83',
'GATE0_PI_ATOMIC_END',
].join('\n');
let sequence = 0;
function sha(value: string | Buffer): string {
return createHash('sha256').update(value).digest('hex');
}
function procStarttime(): number {
const text = readFileSync(`/proc/${process.pid}/stat`, 'utf8');
const close = text.lastIndexOf(')');
const fields = text.slice(close + 2).trim().split(/\s+/);
return Number(fields[19]);
}
function log(event: string, details: Record<string, unknown> = {}): void {
if (!LOG) return;
sequence += 1;
appendFileSync(
LOG,
`${JSON.stringify({ seq: sequence, event, pid: process.pid, starttime_ticks: procStarttime(), ...details })}\n`,
);
}
function argvExtensions(): string[] {
const result: string[] = [];
for (let i = 0; i < process.argv.length; i += 1) {
if (process.argv[i] === '--extension' || process.argv[i] === '-e') {
const candidate = process.argv[i + 1];
if (candidate) result.push(resolve(candidate));
}
}
return result;
}
interface SourceValidation {
ok: boolean;
reason: string;
fragment?: string;
}
function validateSources(): SourceValidation {
const manifestPath = process.env['GATE0_SOURCE_MANIFEST'];
if (!manifestPath) return { ok: true, reason: 'no-manifest-probe-disabled' };
try {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
maxBytes: number;
fragments: Array<{ path: string; sha256: string }>;
};
for (const fragment of manifest.fragments) {
let fileStat;
try {
fileStat = statSync(fragment.path);
} catch {
return { ok: false, reason: 'missing', fragment: fragment.path };
}
if (!fileStat.isFile()) {
return { ok: false, reason: 'not-regular-file', fragment: fragment.path };
}
if (fileStat.size > manifest.maxBytes) {
return { ok: false, reason: 'oversize', fragment: fragment.path };
}
const bytes = readFileSync(fragment.path);
if (sha(bytes) !== fragment.sha256) {
return { ok: false, reason: 'hash-mismatch', fragment: fragment.path };
}
}
return { ok: true, reason: 'all-fragments-valid' };
} catch (error) {
return { ok: false, reason: `manifest-error:${error instanceof Error ? error.name : 'unknown'}` };
}
}
function brokerRequest(payload: Record<string, unknown>): Promise<Record<string, unknown>> {
const socketPath = process.env['GATE0_GENERATION_SOCKET'];
if (!socketPath) return Promise.resolve({ skipped: true });
return new Promise((resolvePromise, reject) => {
const socket = net.createConnection(socketPath);
let buffer = '';
socket.setEncoding('utf8');
socket.on('connect', () => socket.write(`${JSON.stringify(payload)}\n`));
socket.on('data', (chunk) => {
buffer += chunk;
const newline = buffer.indexOf('\n');
if (newline < 0) return;
socket.end();
resolvePromise(JSON.parse(buffer.slice(0, newline)) as Record<string, unknown>);
});
socket.on('error', reject);
});
}
function markerPaths(value: unknown, path = '$'): string[] {
const matches: string[] = [];
if (typeof value === 'string') {
if (value.includes(CONTEXT_BLOCK)) matches.push(path);
return matches;
}
if (Array.isArray(value)) {
value.forEach((item, index) => matches.push(...markerPaths(item, `${path}[${index}]`)));
return matches;
}
if (value && typeof value === 'object') {
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
matches.push(...markerPaths(item, `${path}.${key}`));
}
}
return matches;
}
function assistantToolIds(message: unknown): string[] {
if (!message || typeof message !== 'object') return [];
const candidate = message as { role?: string; content?: unknown };
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return [];
return candidate.content
.filter(
(block): block is { type: 'toolCall'; id: string } =>
Boolean(
block &&
typeof block === 'object' &&
(block as { type?: string }).type === 'toolCall' &&
typeof (block as { id?: unknown }).id === 'string',
),
)
.map((block) => block.id);
}
function assistantText(message: unknown): string {
if (!message || typeof message !== 'object') return '';
const candidate = message as { role?: string; content?: unknown };
if (candidate.role !== 'assistant' || !Array.isArray(candidate.content)) return '';
return candidate.content
.filter(
(block): block is { type: 'text'; text: string } =>
Boolean(
block &&
typeof block === 'object' &&
(block as { type?: string }).type === 'text' &&
typeof (block as { text?: unknown }).text === 'string',
),
)
.map((block) => block.text)
.join('');
}
export default function register(pi: ExtensionAPI) {
const localProviderUrl = process.env['GATE0_LOCAL_PROVIDER_URL'];
if (localProviderUrl) {
pi.registerProvider('gate0-local', {
baseUrl: localProviderUrl,
apiKey: 'gate0-probe-not-a-secret',
api: 'openai-completions',
models: [
{
id: 'gate0-model',
name: 'Gate0 deterministic local model',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_000,
maxTokens: 1_024,
},
],
});
}
const extensions = argvExtensions();
const lastPosition = extensions.length > 0 && extensions.at(-1) === SELF;
interface RequestCycle {
nonce: string;
verified: boolean;
sourceReason: string;
}
let buildingCycle: RequestCycle | undefined;
const inFlightCycles: RequestCycle[] = [];
const toolNonce = new Map<string, { nonce: string; verified: boolean; sourceReason: string }>();
pi.on('session_start', async (event) => {
const broker = await brokerRequest({ action: 'lifecycle', phase: 'start', reason: event.reason });
log('session_start', {
reason: event.reason,
extensions,
self: SELF,
lastPosition,
gateState: lastPosition ? 'UNVERIFIED_READY' : 'CLOSED_NOT_LAST',
broker,
});
});
pi.on('session_shutdown', async (event) => {
const broker = await brokerRequest({ action: 'lifecycle', phase: 'shutdown', reason: event.reason });
log('session_shutdown', { reason: event.reason, broker });
});
pi.on('context', async (event) => {
const validation = validateSources();
buildingCycle = {
nonce: randomUUID(),
sourceReason: validation.reason,
verified: lastPosition && validation.ok,
};
const inputJson = JSON.stringify(event.messages);
const injected = {
role: 'custom' as const,
customType: 'gate0-context',
content: CONTEXT_BLOCK,
display: false,
timestamp: Date.now(),
};
const outputMessages = buildingCycle.verified
? [...event.messages, injected]
: [...event.messages];
const outputPrefix = outputMessages.slice(0, event.messages.length);
const sourceBroker = validation.ok
? { action: 'none', reason: 'source-valid' }
: await brokerRequest({ action: 'source-invalid', reason: validation.reason });
log('context_return', {
requestNonce: buildingCycle.nonce,
sourceValidation: validation,
sourceBroker,
lastPosition,
promotion: false,
injectionDecision: buildingCycle.verified ? 'ONE_ATOMIC_AGENT_MESSAGE' : 'REFUSED',
inputCount: event.messages.length,
outputCount: outputMessages.length,
prefixHashBefore: sha(inputJson),
prefixHashAfter: sha(JSON.stringify(outputPrefix)),
prefixPreservedByReturn: sha(inputJson) === sha(JSON.stringify(outputPrefix)),
blockLength: CONTEXT_BLOCK.length,
blockSha256: sha(CONTEXT_BLOCK),
});
return { messages: outputMessages };
});
pi.on('before_provider_request', async (event) => {
const paths = markerPaths(event.payload);
const cycle = buildingCycle;
buildingCycle = undefined;
if (cycle) inFlightCycles.push(cycle);
log('before_provider_request', {
requestNonce: cycle?.nonce,
inFlightDepth: inFlightCycles.length,
markerOccurrences: paths.length,
markerPaths: paths,
finalPayloadValid: Boolean(cycle?.verified && paths.length === 1),
});
});
pi.on('after_provider_response', async (event) => {
const cycle = inFlightCycles[0];
log('after_provider_response', {
requestNonce: cycle?.nonce,
status: event.status,
assistantContentAvailableAtThisHook: false,
timing: 'headers/status before stream consumption',
});
});
pi.on('message_end', async (event) => {
const role = (event.message as { role?: string }).role;
const ids = assistantToolIds(event.message);
const text = assistantText(event.message);
const cycle = role === 'assistant' ? inFlightCycles.shift() : undefined;
if (ids.length > 0 && cycle) {
for (const id of ids) {
toolNonce.set(id, {
nonce: cycle.nonce,
verified: cycle.verified,
sourceReason: cycle.sourceReason,
});
}
}
log('message_end', {
role,
assistantContentObserved: role === 'assistant',
requestNonce: cycle?.nonce,
inFlightDepthAfter: inFlightCycles.length,
toolCallIds: ids,
nonceMappings: ids.map((id) => ({ toolCallId: id, requestNonce: cycle?.nonce })),
exactContextBlockCopied: text.includes(CONTEXT_BLOCK),
assistantTextSha256: text ? sha(text) : null,
});
});
pi.on('tool_call', async (event) => {
const mapping = toolNonce.get(event.toolCallId);
const allowed = Boolean(lastPosition && mapping?.verified);
log('tool_call', {
toolCallId: event.toolCallId,
toolName: event.toolName,
mapping: mapping ?? null,
allowed,
reason: !lastPosition
? 'closed-not-last'
: !mapping
? 'unknown-tool-call-id'
: !mapping.verified
? `unverified-source:${mapping.sourceReason}`
: 'exact-tool-call-id-mapped-to-verified-request-nonce',
});
if (!allowed) return { block: true, reason: 'Gate0 probe refused unverified tool batch' };
});
pi.on('agent_settled', async () => {
log('agent_settled', { retainedNonceMappingsBeforeClear: toolNonce.size });
toolNonce.clear();
});
pi.registerTool({
name: 'gate0_nonce_probe',
label: 'Gate0 Nonce Probe',
description: 'Gate0-only harmless tool used to prove toolCallId to request-nonce correlation.',
parameters: Type.Object({ label: Type.String() }),
async execute(toolCallId, params) {
const broker = await brokerRequest({ action: 'promote-probe' });
log('tool_execute', { toolCallId, label: params.label, broker });
return {
content: [{ type: 'text', text: `gate0_nonce_probe executed for ${params.label}` }],
details: { harmless: true },
};
},
});
pi.registerCommand('gate0-reload', {
description: 'Trigger a real same-PID Pi extension/runtime reload.',
handler: async (_args, ctx) => {
log('reload_command_before');
await ctx.reload();
return;
},
});
}

View File

@@ -0,0 +1,450 @@
#!/usr/bin/env python3
"""Drive real Pi 0.80.x RPC for P2/P3/P5/P6 runtime evidence."""
from __future__ import annotations
import hashlib
import json
import os
import queue
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import threading
import time
from pathlib import Path
from typing import Any, Callable
HERE = Path(__file__).resolve().parent
BLOCK = "\n".join(
[
"GATE0_PI_ATOMIC_BEGIN",
"segment-01=alpha-7e31",
"segment-02=middle-9c42",
"segment-03=omega-5b83",
"GATE0_PI_ATOMIC_END",
]
)
def wait_path(path: Path, timeout: float = 20) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if path.exists():
return
time.sleep(0.05)
raise TimeoutError(f"timed out waiting for {path}")
def socket_request(path: Path, payload: dict[str, object]) -> None:
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect(str(path))
conn.sendall((json.dumps(payload) + "\n").encode())
conn.makefile("r", encoding="utf-8").readline()
conn.close()
def jsonl(path: Path) -> list[dict[str, Any]]:
if not path.exists():
return []
return [json.loads(line) for line in path.read_text().splitlines() if line]
class PiRpc:
def __init__(self, command: list[str], cwd: Path, env: dict[str, str]):
self.process = subprocess.Popen(
command,
cwd=cwd,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
start_new_session=True,
)
self.events: queue.Queue[dict[str, Any]] = queue.Queue()
self.raw_lines: list[str] = []
self.stderr_lines: list[str] = []
threading.Thread(target=self._read_stdout, daemon=True).start()
threading.Thread(target=self._read_stderr, daemon=True).start()
def _read_stdout(self) -> None:
assert self.process.stdout is not None
for line in self.process.stdout:
stripped = line.rstrip("\n")
self.raw_lines.append(stripped)
try:
event = json.loads(stripped)
except json.JSONDecodeError:
continue
self.events.put(event)
def _read_stderr(self) -> None:
assert self.process.stderr is not None
for line in self.process.stderr:
self.stderr_lines.append(line.rstrip("\n"))
def send(self, payload: dict[str, object]) -> None:
assert self.process.stdin is not None
self.process.stdin.write(json.dumps(payload) + "\n")
self.process.stdin.flush()
def wait(self, predicate: Callable[[dict[str, Any]], bool], description: str, timeout: float = 180) -> dict[str, Any]:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if self.process.poll() is not None and self.events.empty():
raise RuntimeError(
f"Pi exited {self.process.returncode} while waiting for {description}: "
+ " | ".join(self.stderr_lines[-5:])
)
try:
event = self.events.get(timeout=0.2)
except queue.Empty:
continue
if predicate(event):
return event
raise TimeoutError(f"timed out waiting for {description}")
def response(self, request_id: str, timeout: float = 180) -> dict[str, Any]:
return self.wait(
lambda event: event.get("type") == "response" and event.get("id") == request_id,
f"response {request_id}",
timeout,
)
def prompt_and_settle(self, request_id: str, message: str) -> None:
self.send({"id": request_id, "type": "prompt", "message": message})
response = self.response(request_id)
if not response.get("success"):
raise RuntimeError(f"prompt rejected: {response}")
self.wait(lambda event: event.get("type") == "agent_settled", f"agent_settled {request_id}")
def close(self) -> None:
if self.process.poll() is None:
try:
os.killpg(self.process.pid, signal.SIGTERM)
except ProcessLookupError:
pass
try:
self.process.wait(timeout=8)
except subprocess.TimeoutExpired:
os.killpg(self.process.pid, signal.SIGKILL)
self.process.wait(timeout=5)
def manifest(path: Path, fragment: Path, expected_hash: str, max_bytes: int = 64) -> None:
path.write_text(
json.dumps(
{
"maxBytes": max_bytes,
"fragments": [{"path": str(fragment), "sha256": expected_hash}],
},
sort_keys=True,
)
)
def run_open(root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str], list[str]]:
workspace = root / "workspace"
workspace.mkdir()
session_dir = root / "sessions"
session_dir.mkdir()
pi_log = root / "pi-hooks.jsonl"
generation_log = root / "generation.jsonl"
generation_socket = root / "generation.sock"
source_manifest = root / "manifest.json"
valid_fragment = root / "fragment.md"
valid_fragment.write_text("NORMATIVE-FRAGMENT-v1\n")
expected = hashlib.sha256(valid_fragment.read_bytes()).hexdigest()
manifest(source_manifest, valid_fragment, expected)
broker = subprocess.Popen(
[
sys.executable,
str(HERE / "p3_generation_broker.py"),
"--socket",
str(generation_socket),
"--log",
str(generation_log),
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
wait_path(generation_socket)
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(pi_log),
"GATE0_GENERATION_SOCKET": str(generation_socket),
"GATE0_SOURCE_MANIFEST": str(source_manifest),
"GATE0_PI_CONTEXT_BLOCK": BLOCK,
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--session-dir",
str(session_dir),
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--model",
"openai-codex/gpt-5.6-sol",
"--thinking",
"medium",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.send({"id": "state-0", "type": "get_state"})
state0 = pi.response("state-0")
original_session = state0["data"]["sessionFile"]
pi.prompt_and_settle(
"p2",
"Call gate0_nonce_probe exactly once with label p2. After the tool finishes, copy the exact full GATE0_PI_ATOMIC_BEGIN through GATE0_PI_ATOMIC_END block from context, with no commentary.",
)
# P3 immediately follows the valid P2 promotion so reload must revoke a
# genuinely VERIFIED prior generation, not an already-invalid source run.
pi.send({"id": "reload", "type": "prompt", "message": "/gate0-reload"})
reload_response = pi.response("reload")
if not reload_response.get("success"):
raise RuntimeError(f"reload command failed: {reload_response}")
pi.send({"id": "clone", "type": "clone"})
clone_response = pi.response("clone")
if not clone_response.get("success") or clone_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"clone failed: {clone_response}")
pi.send({"id": "new", "type": "new_session"})
new_response = pi.response("new")
if not new_response.get("success") or new_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"new session failed: {new_response}")
pi.send(
{
"id": "resume",
"type": "switch_session",
"sessionPath": original_session,
}
)
resume_response = pi.response("resume")
if not resume_response.get("success") or resume_response.get("data", {}).get("cancelled"):
raise RuntimeError(f"resume failed: {resume_response}")
# P5 missing fragment: action-time source validation must revoke/refuse.
manifest(source_manifest, root / "absent-fragment.md", expected)
pi.prompt_and_settle(
"p5-missing",
"Call gate0_nonce_probe exactly once with label p5-missing, then stop.",
)
# P5 oversize fragment: expected hash is correct, size limit is not.
oversize = root / "oversize.md"
oversize.write_text("X" * 65)
manifest(source_manifest, oversize, hashlib.sha256(oversize.read_bytes()).hexdigest(), 64)
pi.prompt_and_settle(
"p5-oversize",
"Call gate0_nonce_probe exactly once with label p5-oversize, then stop.",
)
# P5 hash mismatch: size is valid but bytes differ from expected.
mismatch = root / "mismatch.md"
mismatch.write_text("tampered\n")
manifest(source_manifest, mismatch, expected, 64)
pi.prompt_and_settle(
"p5-hash",
"Call gate0_nonce_probe exactly once with label p5-hash-mismatch, then stop.",
)
time.sleep(1)
return jsonl(pi_log), jsonl(generation_log), list(pi.raw_lines), list(pi.stderr_lines)
finally:
pi.close()
try:
socket_request(generation_socket, {"action": "shutdown-broker"})
except OSError:
pass
try:
broker.wait(timeout=5)
except subprocess.TimeoutExpired:
broker.kill()
broker.wait()
def run_closed(root: Path) -> list[dict[str, Any]]:
workspace = root / "closed-workspace"
workspace.mkdir()
pi_log = root / "closed-hooks.jsonl"
env = os.environ.copy()
env.update(
{
"GATE0_PI_LOG": str(pi_log),
"MOSAIC_PI_FORCE_SKILLS": "",
"PI_SKIP_VERSION_CHECK": "1",
}
)
command = [
"mosaic",
"yolo",
"pi",
"--mode",
"rpc",
"--no-session",
"--no-extensions",
"--no-context-files",
"--no-prompt-templates",
"--extension",
str(HERE / "pi_gate0_extension.ts"),
"--extension",
str(HERE / "pi_later_extension.ts"),
]
pi = PiRpc(command, workspace, env)
try:
pi.send({"id": "closed-state", "type": "get_state"})
pi.response("closed-state")
time.sleep(0.5)
return jsonl(pi_log)
finally:
pi.close()
def main() -> None:
with tempfile.TemporaryDirectory(prefix="gate0-pi-") as temp:
root = Path(temp)
records, generations, rpc_lines, stderr_lines = run_open(root)
closed = run_closed(root)
p2_message = next(
r for r in records if r["event"] == "message_end" and r.get("nonceMappings")
)
p2_tool = next(r for r in records if r["event"] == "tool_call" and r.get("allowed"))
mapped = p2_message["nonceMappings"][0]
assert mapped["toolCallId"] == p2_tool["toolCallId"]
assert mapped["requestNonce"] == p2_tool["mapping"]["nonce"]
assert next(r for r in records if r["event"] == "session_start")["lastPosition"] is True
assert next(r for r in closed if r["event"] == "session_start")["gateState"] == "CLOSED_NOT_LAST"
reload_revoke = next(
r
for r in generations
if r["event"] == "runtime_generation_bump"
and r.get("reason") == "reload"
and r.get("phase") == "shutdown"
)
assert reload_revoke["prior_lease"] == "VERIFIED"
assert reload_revoke["prior_lease_revoked"] is True
for reason in {"missing", "oversize", "hash-mismatch"}:
assert any(
r["event"] == "context_return"
and r.get("sourceValidation", {}).get("reason") == reason
and r.get("injectionDecision") == "REFUSED"
and r.get("promotion") is False
for r in records
)
assert any(
r["event"] == "tool_call"
and r.get("mapping", {}).get("sourceReason") == reason
and r.get("allowed") is False
for r in records
)
assert any(
r["event"] == "message_end" and r.get("exactContextBlockCopied") is True
for r in records
)
print("$ python3 docs/compaction-refresh/probes/pi_gate0_run.py")
print("machine_assertions=PASS")
print("runtime_versions:")
print(" " + subprocess.check_output(["pi", "--version"], text=True).strip())
print(" " + subprocess.check_output(["mosaic", "--version"], text=True).strip())
print("\nP2_EVENT_ORDER_AND_NONCE_MAP:")
for record in records:
if record["seq"] <= 12 and record["event"] in {
"after_provider_response",
"message_end",
"tool_call",
"tool_execute",
} and (
record["event"] != "message_end"
or record.get("role") == "assistant"
):
print(json.dumps(record, sort_keys=True))
print("\nP2_LAST_OR_CLOSED:")
print(json.dumps(next(r for r in records if r["event"] == "session_start"), sort_keys=True))
print(json.dumps(next(r for r in closed if r["event"] == "session_start"), sort_keys=True))
print("\nP3_GENERATION_BROKER:")
for record in generations:
if record["event"] in {"probe_lease_promoted", "runtime_generation_bump"}:
print(json.dumps(record, sort_keys=True))
print("\nP5_SOURCE_INVALIDATION:")
fault_reasons = {"missing", "oversize", "hash-mismatch"}
emitted_context: set[str] = set()
emitted_tool: set[str] = set()
for record in records:
source_reason = record.get("sourceValidation", {}).get("reason")
if (
record["event"] == "context_return"
and source_reason in fault_reasons
and source_reason not in emitted_context
):
print(json.dumps(record, sort_keys=True))
emitted_context.add(source_reason)
mapping_reason = record.get("mapping", {}).get("sourceReason")
if (
record["event"] == "tool_call"
and not record.get("allowed")
and mapping_reason in fault_reasons
and mapping_reason not in emitted_tool
):
print(json.dumps(record, sort_keys=True))
emitted_tool.add(mapping_reason)
emitted_broker: set[str] = set()
for record in generations:
reason = record.get("source_reason")
if record["event"] == "source_invalidation_revoke" and reason not in emitted_broker:
print(json.dumps(record, sort_keys=True))
emitted_broker.add(str(reason))
print("\nP6_PI_CONTEXT_ATOMIC_OBSERVATION:")
for record in records:
include = (
(record["event"] == "context_return" and record.get("injectionDecision") == "ONE_ATOMIC_AGENT_MESSAGE")
or (record["event"] == "before_provider_request" and record.get("finalPayloadValid"))
or (record["event"] == "message_end" and record.get("exactContextBlockCopied"))
)
if include and record["seq"] <= 12:
print(json.dumps(record, sort_keys=True))
print("\nRPC_EVENT_COUNTS:")
counts: dict[str, int] = {}
for line in rpc_lines:
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
key = str(event.get("type"))
counts[key] = counts.get(key, 0) + 1
print(json.dumps(counts, sort_keys=True))
print("stderr_nonempty=" + str(bool(stderr_lines)))
for line in stderr_lines[:10]:
print("stderr: " + line[:500])
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,8 @@
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
// Deliberately loaded after pi_gate0_extension.ts. The Gate0 extension must
// observe its argv position and remain CLOSED rather than claiming finality.
export default function register(pi: ExtensionAPI) {
pi.on('context', async (event) => ({ messages: [...event.messages] }));
pi.on('before_provider_request', async () => undefined);
}