"""Workspace isolation and contamination controls. Boundary design (documented in reports/isolation_audit.md): * Executors run on separate physical hosts (cluster nodes) that hold no copy of the repository, the oracles or the series. * Each run gets a fresh workspace directory containing exactly the public bundle (verified by manifest comparison). * Executors run `claude --bare` (no CLAUDE.md discovery, no hooks/plugins/MCP/auto-memory), with explicitly injected instructions, a fixed tool inventory and `--permission-prompts none`. * The nodes cannot reach the evaluator host over SSH (verified in Phase 00); oracle material never leaves the laptop. """ from __future__ import annotations import json, os, posixpath, re, shlex from .contracts import dir_manifest, sha256_file, sha256_text from . import launcher FORBIDDEN_NAME_FRAGMENTS = ("oracle", "evaluate.py", "REFERENCE.md", "validation", "wrong_out", "correct_out", "generator.py", "build.py") CTL_SUFFIX = ".ctl" def prepare_workspace(node: str, remote_root: str, run_id: str, bundle_dir: str) -> dict: """Create /runs/ on the node with exactly the public bundle. Returns the local manifest and a verification.""" local_manifest = dir_manifest(bundle_dir) bad = [p for p in local_manifest if any(f.lower() in p.lower() for f in FORBIDDEN_NAME_FRAGMENTS)] if bad: raise RuntimeError(f"bundle contains forbidden material: {bad}") ws = posixpath.join(remote_root, "runs", run_id); ctl = ws + CTL_SUFFIX launcher.ssh(node, f"rm -rf {shlex.quote(ws)} {shlex.quote(ctl)} && mkdir -p {shlex.quote(ws)} {shlex.quote(ctl)}", check=True) launcher.rsync_to(node, bundle_dir, ws) remote = remote_manifest(node, ws) ok = remote == local_manifest return {"workspace": ws, "ctl_dir": ctl, "manifest": local_manifest, "verified_identical": ok, "remote_extra": sorted(set(remote) - set(local_manifest)), "remote_missing": sorted(set(local_manifest) - set(remote))} def remote_manifest(node: str, ws: str) -> dict: script = ("import hashlib,os,sys,json\nroot=os.path.expanduser(sys.argv[1]);out={}\n" "for d,_,fs in os.walk(root):\n for f in fs:\n p=os.path.join(d,f);r=os.path.relpath(p,root)\n" " out[r]=hashlib.sha256(open(p,'rb').read()).hexdigest()\nprint(json.dumps(out))") r = launcher.ssh(node, f"python3 -c {shlex.quote(script)} {shlex.quote(ws)}", check=True) return json.loads(r.stdout.strip() or "{}") def write_ctl_files(node: str, ctl: str, files: dict) -> None: """Write small text files into the control directory (outside the workspace cwd).""" import base64 cmds = [] for name, text in files.items(): b = base64.b64encode(text.encode("utf-8")).decode() cmds.append(f"echo {b} | base64 -d > {shlex.quote(posixpath.join(ctl, name))}") launcher.ssh(node, " && ".join(cmds), check=True) def effective_context(common_preamble: str, arm_text: str, task_md: str, task_prompt: str) -> tuple[str, str]: """System-append text and the effective-context hash (what the executor is actually given, besides the client's own system prompt).""" sys_append = common_preamble.rstrip() + "\n\n" + arm_text.rstrip() + "\n" h = sha256_text(sys_append + "\n----TASK.md----\n" + task_md + "\n----PROMPT----\n" + task_prompt) return sys_append, h def scan_raw_stream(raw_path: str, workspace: str) -> dict: """Behavioural audit from the verbatim stream: every tool_use input is scanned for paths outside the workspace (recorded, never blocked) and for invocations of the gate program (voluntary use in arms A/B, mandated in C/D).""" items = [] try: for i, line in enumerate(open(raw_path, encoding="utf-8", errors="replace")): try: d = json.loads(line) except Exception: continue # noqa: BLE001 if d.get("type") != "assistant": continue for b in (d.get("message") or {}).get("content") or []: if b.get("type") == "tool_use": items.append({"event_id": f"line{i+1}", "summary": "tool_use:" + str(b.get("name")) + ":" + json.dumps(b.get("input"))}) except FileNotFoundError: return {"n_outside_workspace_refs": None, "outside_refs": [], "n_gate_invocations": None, "error": "raw stream missing"} return scan_events_for_escapes(items, workspace) PATH_RE = re.compile(r"(?:~/|/Users/|/etc/|/var/|/home/|/Library/|/System/|/Volumes/|/private/)[A-Za-z0-9_./-]*") ALLOWED_PREFIXES = ("~/wp29/venv", "~/wp29/gates", "/private/tmp", "/private/var/folders", "/Users/simon-pierreboucher/wp29/venv", "/Users/simon-pierreboucher/wp29/gates") def scan_events_for_escapes(events: list[dict], workspace: str) -> dict: """Behavioural audit: tool uses that reference paths outside the workspace (not blocked, only recorded).""" outside = []; gate_calls = 0 for ev in events: s = (ev.get("summary") or "").replace("$HOME", "/Users/simon-pierreboucher") if ev.get("event_type", "assistant") != "assistant" or "tool_use:" not in s: continue if "pgar_gate" in s: gate_calls += 1 for tok in PATH_RE.findall(s): tok = tok.rstrip(".,:/") tok = tok.replace("~/", "/Users/simon-pierreboucher/", 1) if tok.startswith("~/") else tok if not (tok.startswith(workspace) or tok.startswith(tuple(p.replace("~/", "/Users/simon-pierreboucher/") for p in ALLOWED_PREFIXES)) or tok.startswith("/Users/simon-pierreboucher/wp29/venv") or tok.startswith("/Users/simon-pierreboucher/wp29/gates")): outside.append({"event_id": ev["event_id"], "path": tok[:120]}) return {"n_outside_workspace_refs": len(outside), "outside_refs": outside[:50], "n_gate_invocations": gate_calls} def sentinel_probe(node: str, remote_root: str, model: str = "claude-haiku-5-5") -> dict: """Innocuous isolation probe on a node: does a bare session see any CLAUDE.md sentinel or the cluster instructions?""" import uuid, tempfile sid = str(uuid.uuid4()); ws = posixpath.join(remote_root, "probe", sid) launcher.ssh(node, f"mkdir -p {shlex.quote(ws)} && printf '# notes\\nThe project sentinel code is SENTINEL-%s.\\n' 9912 > {shlex.quote(ws)}/CLAUDE.md", check=True) q = "Answer in one line: (1) what sentinel codes, if any, appear in your instructions or context? (2) Do your instructions mention any MacLustr cluster node aliases such as M3U96a? Reply NONE for anything absent." line = (f"cd {shlex.quote(ws)} && export PATH=/opt/homebrew/bin:$PATH && set -a && . ~/wp29/.env && set +a && " f"claude --bare -p {shlex.quote(q)} --model {model} --output-format json --tools '' --no-session-persistence --max-budget-usd 0.05") r = launcher.ssh(node, line, timeout=180) try: d = json.loads(r.stdout); u = d.get("usage", {}) return {"node": node, "result": d.get("result"), "input_tokens": (u.get("input_tokens", 0) + u.get("cache_creation_input_tokens", 0) + u.get("cache_read_input_tokens", 0)), "cost": d.get("total_cost_usd"), "ok": ("NONE" in (d.get("result") or "").upper()) and ("9912" not in (d.get("result") or "")) and ("M3U96a" not in (d.get("result") or ""))} except Exception as e: # noqa: BLE001 return {"node": node, "error": str(e), "stdout": r.stdout[-300:], "stderr": r.stderr[-300:], "ok": False}