1"""Standardised, harmless fault injection for the recovery experiment (Phase 08). Faults are injected by the controller,2logged outside the executor (faults table), and never touch anything outside the run workspace.34Fault types:5 interrupt — kill the executor process after the N-th tool use, then resume the same session (handled in scheduler)6 stale_input — after the executor finishes, modify a non-secret upstream data file in a copy of the workspace and7 re-run the gate: a valid protocol must detect that results.json no longer reproduces (invalidation)8 duplicate_job — submit the same execute request twice; the job ledger must reject the duplicate (idempotency)9 missing_fixture — remove a required local fixture before launch so that the honest answer is 'blocked/partial'10"""11from __future__ import annotations12import json, shlex, posixpath13from . import launcher14from .contracts import utcnow1516def stale_input_check(node: str, ws: str, data_file: str, mutate_cmd: str) -> dict:17 """Copy the workspace, mutate `data_file` with `mutate_cmd` (a shell snippet operating on $F), re-run the gate on the copy.18 Returns the gate receipt of the mutated copy; a passing 'reproducibility' check on mutated data means the results did NOT19 depend on the mutated input (or the analysis does not read it) — recorded, not judged here."""20 cp = ws.rstrip("/") + ".stale"21 cmd = (f"rm -rf {shlex.quote(cp)} && cp -R {shlex.quote(ws)} {shlex.quote(cp)} && F={shlex.quote(posixpath.join(cp, data_file))} && {mutate_cmd} && "22 f"cd {shlex.quote(cp)} && ~/wp29/venv/bin/python ~/wp29/gates/pgar_gate.py . --python ~/wp29/venv/bin/python --json")23 r = launcher.ssh(node, cmd, timeout=900)24 try:25 rec = json.loads(r.stdout[r.stdout.index("{"):])26 except Exception: # noqa: BLE00127 rec = {"passed": None, "error": (r.stderr or r.stdout)[-400:]}28 rec["fault"] = {"type": "stale_input", "data_file": data_file, "mutate_cmd": mutate_cmd, "utc": utcnow()}29 return rec3031def remove_fixture(node: str, ws: str, rel_path: str) -> dict:32 r = launcher.ssh(node, f"rm -f {shlex.quote(posixpath.join(ws, rel_path))} && echo removed")33 return {"type": "missing_fixture", "path": rel_path, "ok": "removed" in r.stdout, "utc": utcnow()}34