Files
coyote/assets/agents/adversary/scripts/run_checks.py
T
Dark-Alex-17 48753eaa46 fix(agents): scrub GRAPH_STATE* from adversary verification commands; raise review-suite max_agent_depth
FIX A: run_checks.py passed no env= to Popen, so verification commands
inherited the live GRAPH_STATE/GRAPH_STATE_FILE. Every suite script's
load_state() prefers GRAPH_STATE_FILE, so when the adversary reviews a repo
whose own tests exec these scripts (this repo's R5 tests) the leaked file
silently overrode the test state -> false failures. Popen now gets an env
with all GRAPH_STATE* keys removed. The five sibling script-test helpers
(review-gauntlet, code-reviewer, finding-verifier, step-runner,
deep-research) gain .env_remove("GRAPH_STATE_FILE") for hermeticity. New
R5 test loads state via GRAPH_STATE_FILE and asserts a shell-neutral
python3 -c probe sees both vars UNSET.

FIX B: the depth check compares the child's absolute depth against the
SPAWNER's own max_agent_depth (root = 0), so under
architect(0)->sisyphus(1)->review-gauntlet(2)->code-reviewer(3) the
code-review lane faulted "Max agent depth exceeded (4/3)". Limits are now
child depth + 1 headroom along the documented chains, with the chain in a
comment next to each key: code-reviewer 3->5, domain-reviewer 2->6,
architecture-reviewer 2->4, step-runner default->4 (explicit),
review-gauntlet stays 4. domain-reviewer's file-reviewer hop is documented
but inert (can_spawn_agents unset) - left unwired by decision; noted in
config and pinned via a DOCUMENTED_BUT_UNWIRED exemption in the new
bundled-config test, which pins every value and asserts per-hop headroom +
reachability along four chains.
2026-09-14 10:50:34 -06:00

153 lines
5.3 KiB
Python

#!/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).
Verification commands run with GRAPH_STATE* (GRAPH_STATE, GRAPH_STATE_FILE)
scrubbed from their environment: the reviewed repo's own tests may exec these very
scripts with their own GRAPH_STATE, and load_state() prefers
GRAPH_STATE_FILE, so an inherited live state file would silently win.
"""
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 {}
env = {k: v for k, v in os.environ.items() if not k.startswith("GRAPH_STATE")}
p = subprocess.Popen(
cmd,
shell=True,
cwd=proj,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
**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"
)
}
)
)