#!/usr/bin/env python3 """Isolated PTY frontend checks; never submit a prompt or restart an existing server.""" import argparse import json import os from pathlib import Path import re import shutil import subprocess import tempfile import time import unicodedata def palette_color(index): basic = [(0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192), (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)] if index < 16: return basic[index] if index < 232: value = index - 16 levels = [0, 95, 135, 175, 215, 255] return levels[value // 36], levels[(value // 6) % 6], levels[value % 6] return (8 + (index - 232) * 10,) * 3 def color_matches(prefix, wanted): fg = None for seq in re.findall(r"\x1b\[([0-9;]*)m", prefix): values = [int(n or 0) for n in seq.split(";")] i = 0 while i < len(values): n = values[i] if n in (0, 39): fg = None elif n == 38 and values[i + 1:i + 2] == [5] and len(values) > i + 2: fg = palette_color(values[i + 2]) i += 2 elif n == 38 and values[i + 1:i + 2] == [2] and len(values) > i + 4: fg = tuple(values[i + 2:i + 5]) i += 4 elif 30 <= n <= 37: fg = palette_color(n - 30) elif 90 <= n <= 97: fg = palette_color(n - 90 + 8) i += 1 return fg == wanted def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--label", required=True, help="Approved expected execution-server label, not the terminal hostname") parser.add_argument("--color", required=True, help="#RRGGBB") parser.add_argument("--model-text", required=True, help="Text visibly identifying the selected model") parser.add_argument("--directory-text", default="~") parser.add_argument("--opencode", default=shutil.which("opencode") or str(Path.home() / ".opencode/bin/opencode")) parser.add_argument("--session", help="Existing idle session; omitted means only home checks") parser.add_argument("--conversation-text", help="Visible title/text proving that --session opened a conversation, not home") parser.add_argument("--server", help="Explicit remote server; normal OpenCode authentication is retained") parser.add_argument("--standalone", action="store_true", help="For an explicitly isolated test sandbox only") parser.add_argument("--output", type=Path, required=True, help="Private local evidence directory; may contain screen text") args = parser.parse_args() if not re.fullmatch(r"#[0-9a-fA-F]{6}", args.color): parser.error("--color must be #RRGGBB") if args.server and args.standalone: parser.error("--server and --standalone cannot be combined") if args.session and not args.conversation_text: parser.error("--session requires --conversation-text to avoid falsely passing a home screen as a conversation") if not shutil.which("tmux"): parser.error("tmux is needed only for this verifier, not for the installed footer") os.umask(0o077) args.output.mkdir(mode=0o700, parents=True, exist_ok=True) if args.output.is_symlink() or args.output.stat().st_uid != os.getuid() or args.output.stat().st_mode & 0o077: parser.error("Evidence directory must be private, non-symlinked and owned by the current user") esc = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") rgb = tuple(bytes.fromhex(args.color[1:])) results = [] # A short socket path is required by Unix-domain socket length limits. with tempfile.TemporaryDirectory(prefix="identity-pty-") as temporary: socket = str(Path(temporary) / "tmux.sock") def tmux(*command, check=True): return subprocess.run(["tmux", "-S", socket, *command], text=True, capture_output=True, check=check, timeout=10).stdout def columns(line): return sum(0 if unicodedata.combining(c) else 2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in line) try: for width in (80, 120): for view in (["home", "conversation"] if args.session else ["home"]): name = f"{view}-{width}" command = [args.opencode] if args.server: command += ["--server", args.server] if args.standalone: command += ["--standalone"] if view == "conversation": command += ["--session", args.session] tmux("-f", "/dev/null", "new-session", "-d", "-s", name, "-x", str(width), "-y", "32", "-c", str(Path.cwd()), *command) tmux("set-option", "-g", "status", "off") deadline = time.monotonic() + 45 while True: capture = tmux("capture-pane", "-p", "-e", "-t", name) plain = esc.sub("", capture) if args.label in plain and args.model_text in plain and (view == "home" or args.conversation_text in plain): break if time.monotonic() > deadline: (args.output / f"{name}-failed.ansi").write_text(capture) raise AssertionError(f"{name}: label/model missing; private capture retained") time.sleep(.25) (args.output / f"{name}.ansi").write_text(capture) assert plain.count(args.label) == 1, "Duplicate label" assert all(columns(line.rstrip()) <= width for line in plain.splitlines()), "Layout overflow" row = next(line for line in capture.splitlines() if args.label in esc.sub("", line)) prefix, rest = row.split(args.label, 1) assert color_matches(prefix, rgb), "Color mismatch; terminal palette/quantization may require visual verification" assert "\x1b[" in rest, "Color reset missing" row_number = next(i + 1 for i, line in enumerate(plain.splitlines()) if args.label in line) assert row_number >= 20, "Label not near footer" assert args.directory_text in plain, "Directory information missing" assert tmux("show-option", "-gv", "status").strip() == "off" tmux("send-keys", "-l", "-t", name, "footer verification (unsent)") deadline = time.monotonic() + 5 while True: draft = tmux("capture-pane", "-p", "-e", "-t", name) draft_plain = esc.sub("", draft) if "footer verification (unsent)" in draft_plain: break if time.monotonic() > deadline: raise AssertionError("Unsent draft missing") time.sleep(.1) assert draft_plain.count(args.label) == 1, "Draft label missing/duplicated" assert all(columns(line.rstrip()) <= width for line in draft_plain.splitlines()), "Draft layout overflow" (args.output / f"{name}-draft.ansi").write_text(draft) results.append({"case": name, "label": args.label, "requested_color": args.color, "row": row_number, "unsent_draft": True, "passed": True}) tmux("send-keys", "-t", name, "C-c", check=False) tmux("kill-session", "-t", name, check=False) finally: tmux("kill-server", check=False) (args.output / "results.json").write_text(json.dumps(results, indent=2) + "\n") print(json.dumps(results, indent=2)) if __name__ == "__main__": main()