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