#!/usr/bin/env python3 """Deterministic execution-evidence recorder for the adversary graph. Runs ONLY the caller-declared verification commands (verification_commands) in the project dir and records the outcomes as `exec_results` — a list of {cmd, exit, duration_s, tail} records. Criterion branches cite this record; they never execute anything themselves. NO auto-detection: undeclared commands mean a "none declared" marker (fail-visible, not fail-guessed). Any runner error degrades into an ENVIRONMENT marker — this script never fails the node. Budget: commands run sequentially, so TOTAL runtime is bounded by a deadline (TOTAL_DEADLINE_SECS = 3300s, the run_checks node's 3600s timeout minus margin). Without it, a handful of hanging commands (4 x 900s) would blow the NODE timeout and the executor would kill this script from OUTSIDE — the in-script degradation would never fire. Each command gets min(PER_COMMAND_TIMEOUT_SECS, remaining budget); commands the deadline leaves no budget for are recorded as {"cmd", "skipped": "deadline"} entries, so the in-script handling stays authoritative. The env var ADVERSARY_RUN_CHECKS_DEADLINE_SECS overrides the deadline (test seam). Process hygiene: commands run through the shell. On POSIX each command gets its own session (start_new_session) and a timeout SIGKILLs the whole process group, so grandchildren of a hung command cannot linger. On non-POSIX platforms only the direct shell child is killed — grandchildren of a timed-out command may survive (documented limitation). """ import json import os import signal import subprocess import time PER_COMMAND_TIMEOUT_SECS = 900 TOTAL_DEADLINE_SECS = float(os.environ.get("ADVERSARY_RUN_CHECKS_DEADLINE_SECS") or 3300) TAIL_LINES = 50 MAX_TAIL_CHARS = 8000 def load_state(): if path := os.environ.get("GRAPH_STATE_FILE"): with open(path) as f: return json.load(f) return json.loads(os.environ.get("GRAPH_STATE", "{}")) def tail_of(text): tail = "\n".join(text.splitlines()[-TAIL_LINES:]) if len(tail) > MAX_TAIL_CHARS: tail = "…" + tail[-MAX_TAIL_CHARS:] return tail def run_one(cmd, proj, budget): """Run one command, hard-bounded by `budget` seconds; return its record.""" start = time.monotonic() popen_kwargs = {"start_new_session": True} if os.name == "posix" else {} p = subprocess.Popen( cmd, shell=True, cwd=proj, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, **popen_kwargs, ) try: out_s, err_s = p.communicate(timeout=budget) except subprocess.TimeoutExpired: if os.name == "posix": try: os.killpg(os.getpgid(p.pid), signal.SIGKILL) except (ProcessLookupError, PermissionError): pass else: p.kill() p.communicate() # reap the child and drain the pipes return { "cmd": cmd, "exit": -1, "duration_s": round(time.monotonic() - start, 1), "tail": f"TIMEOUT: no exit within {budget:.0f}s — recorded as a failing run", } combined = (out_s or "") + (("\n" + err_s) if err_s else "") return { "cmd": cmd, "exit": p.returncode, "duration_s": round(time.monotonic() - start, 1), "tail": tail_of(combined), } def main(): state = load_state() declared = state.get("verification_commands") cmds = ( [c.strip() for c in declared if isinstance(c, str) and c.strip()] if isinstance(declared, list) else [] ) if not cmds: print( json.dumps( { "exec_results": ( "none declared — the caller supplied no verification_commands; " "execution criteria (build/test/lint) cannot be independently " "confirmed. Declare the exact commands in the spawn prompt to " "get recorded runs." ) } ) ) return proj = os.path.expanduser( (state.get("project_dir") or "").strip() or "." ) deadline = time.monotonic() + TOTAL_DEADLINE_SECS results = [] for cmd in cmds: remaining = deadline - time.monotonic() if remaining <= 0: results.append({"cmd": cmd, "skipped": "deadline"}) continue results.append(run_one(cmd, proj, min(PER_COMMAND_TIMEOUT_SECS, remaining))) print(json.dumps({"exec_results": results})) try: main() except Exception as e: # noqa: BLE001 — never fail the node; degrade visibly print( json.dumps( { "exec_results": ( f"ENVIRONMENT: verification runner error: {e} — the declared " "commands could not be executed; treat execution criteria as " "PARTIAL (unproven), not as failed" ) } ) )