Rhodawk Agent
fix(playbook §10): resolve all 12 diagnostic warnings W-001..W-012\n\n W-001 CRITICAL: Add scripts/generate_stubs.sh + Makefile to generate\n openclaude_grpc/openclaude_pb2*.py locally without a full Docker build.\n W-002 HIGH: Delete dead Rhodawk_AI_Pitch_Deck_2026.pptx (operator request).\n W-003 HIGH: Rename mcp_config.json -> mcp_config.ARCHIVE.json with archive notice.\n W-004 HIGH: formal_verifier.py — RHODAWK_Z3_ENABLED defaults to true; loud warning if skipped.\n W-005 MEDIUM: Surface RHODAWK_AUTO_MERGE state in System Status banner.\n W-006 MEDIUM: training_store.PgConn.executescript() splits multi-stmt SQL.\n W-007 MEDIUM: webhook_server returns 403 + JSON when secret unset.\n W-008 MEDIUM: hermes_orchestrator — new HERMES_PROVIDER routing flag.\n W-009 MEDIUM: New night_hunt_lock.py shared mutex across both night-hunt loops.\n W-010 MEDIUM: repo_harvester uses dynamic 30-day rolling pushed window.\n W-011 MEDIUM: lora_scheduler emits Telegram/Slack notification on export.\n W-012 MEDIUM: Live Operations tab shows System Status feature-gate banner.
6ed9a00 | """ | |
| Rhodawk AI — Lightweight Formal Verification Gate | |
| ================================================= | |
| Uses Z3 (SMT solver) to perform bounded symbolic verification of | |
| simple integer arithmetic, array bounds, and null-safety properties | |
| extracted from Python diffs. | |
| This is NOT a full program verifier. It covers: | |
| 1. Array/list index bounds — catches IndexError when indices are computable | |
| 2. Integer arithmetic — overflow / divide-by-zero on constant expressions | |
| 3. Assert statement reachability — checks user asserts are satisfiable | |
| For complex code (loops, recursion, string ops) it returns SKIP, which does | |
| NOT block the diff — Z3 gate is advisory, not blocking, unless a definitive | |
| UNSAFE result is obtained. | |
| Install: z3-solver (pip install z3-solver) | |
| Enable: RHODAWK_Z3_ENABLED=true | |
| """ | |
| import os | |
| import re | |
| # W-004 FIX: Z3 formal verification gate is now ON by default. | |
| # z3-solver is already pinned in requirements.txt. Operators may explicitly | |
| # disable it with RHODAWK_Z3_ENABLED=false. A loud startup warning is emitted | |
| # whenever Z3 is skipped (either disabled or import failure). | |
| Z3_ENABLED = os.getenv("RHODAWK_Z3_ENABLED", "true").lower() == "true" | |
| _IMPORT_OK = False | |
| try: | |
| import z3 as _z3 | |
| _IMPORT_OK = True | |
| except ImportError: | |
| pass | |
| import sys as _sys | |
| if not Z3_ENABLED: | |
| print( | |
| "[STARTUP WARNING] Z3 formal verification gate is DISABLED " | |
| "(RHODAWK_Z3_ENABLED=false). Step 7b of the healing loop will " | |
| "be skipped and no UNSAFE diffs will be blocked by Z3.", | |
| file=_sys.stderr, | |
| ) | |
| elif not _IMPORT_OK: | |
| print( | |
| "[STARTUP WARNING] Z3 formal verification gate is ENABLED but " | |
| "z3-solver is not installed. Run: pip install z3-solver. " | |
| "Step 7b of the healing loop will return SKIP.", | |
| file=_sys.stderr, | |
| ) | |
| def _extract_added_lines(diff_text: str) -> list[str]: | |
| return [ | |
| line[1:].strip() | |
| for line in diff_text.splitlines() | |
| if line.startswith("+") and not line.startswith("+++") | |
| ] | |
| def _check_divide_by_zero(lines: list[str]) -> list[str]: | |
| """Detect literal divide-by-zero: x / 0 or x % 0.""" | |
| issues = [] | |
| div_pat = re.compile(r"\b(\w+)\s*/\s*0\b") | |
| mod_pat = re.compile(r"\b(\w+)\s*%\s*0\b") | |
| for line in lines: | |
| if div_pat.search(line): | |
| issues.append(f"Literal divide-by-zero: {line}") | |
| if mod_pat.search(line): | |
| issues.append(f"Literal modulo-by-zero: {line}") | |
| return issues | |
| def _check_index_bounds(lines: list[str]) -> list[str]: | |
| """ | |
| Detect patterns like arr[N] where N is a literal integer and can use Z3 | |
| to verify the index is non-negative. For constant-length list literals | |
| we also check upper bound. | |
| """ | |
| if not _IMPORT_OK: | |
| return [] | |
| issues = [] | |
| idx_pat = re.compile(r"\b\w+\s*\[\s*(-?\d+)\s*\]") | |
| for line in lines: | |
| for m in idx_pat.finditer(line): | |
| idx_val = int(m.group(1)) | |
| solver = _z3.Solver() | |
| i = _z3.Int("i") | |
| solver.add(i == idx_val) | |
| solver.add(i < 0) | |
| if solver.check() == _z3.sat: | |
| issues.append(f"Negative literal index [{idx_val}]: {line}") | |
| return issues | |
| def _check_assert_satisfiability(lines: list[str]) -> list[str]: | |
| """ | |
| Check assert statements with simple integer inequalities using Z3. | |
| assert x > 0 where x appears to be assigned a negative literal. | |
| """ | |
| if not _IMPORT_OK: | |
| return [] | |
| issues = [] | |
| assign_pat = re.compile(r"(\w+)\s*=\s*(-?\d+)") | |
| assert_pat = re.compile(r"assert\s+(\w+)\s*([><=!]+)\s*(-?\d+)") | |
| assignments: dict[str, int] = {} | |
| for line in lines: | |
| for m in assign_pat.finditer(line): | |
| assignments[m.group(1)] = int(m.group(2)) | |
| for line in lines: | |
| m = assert_pat.search(line) | |
| if m: | |
| var, op, val_str = m.group(1), m.group(2), m.group(3) | |
| if var in assignments: | |
| lhs = assignments[var] | |
| rhs = int(val_str) | |
| solver = _z3.Solver() | |
| x = _z3.Int("x") | |
| solver.add(x == lhs) | |
| op_map = { | |
| ">": x > rhs, ">=": x >= rhs, "<": x < rhs, | |
| "<=": x <= rhs, "==": x == rhs, "!=": x != rhs, | |
| } | |
| constraint = op_map.get(op) | |
| if constraint is not None: | |
| solver.add(_z3.Not(constraint)) | |
| if solver.check() == _z3.sat: | |
| issues.append( | |
| f"Assert always fails: `{var} {op} {rhs}` " | |
| f"but {var}={lhs}: {line}" | |
| ) | |
| return issues | |
| def run_formal_verification(diff_text: str) -> dict: | |
| """ | |
| Run Z3-backed formal verification on the diff. | |
| Returns: | |
| { | |
| "verdict": "SAFE" | "UNSAFE" | "SKIP", | |
| "issues": list[str], | |
| "summary": str, | |
| } | |
| """ | |
| if not Z3_ENABLED: | |
| return {"verdict": "SKIP", "issues": [], "summary": "Z3 verification disabled"} | |
| if not _IMPORT_OK: | |
| return { | |
| "verdict": "SKIP", | |
| "issues": [], | |
| "summary": "z3-solver not installed (pip install z3-solver)", | |
| } | |
| python_lines = _extract_added_lines(diff_text) | |
| if not python_lines: | |
| return {"verdict": "SKIP", "issues": [], "summary": "No added lines to verify"} | |
| all_issues: list[str] = [] | |
| try: | |
| all_issues.extend(_check_divide_by_zero(python_lines)) | |
| all_issues.extend(_check_index_bounds(python_lines)) | |
| all_issues.extend(_check_assert_satisfiability(python_lines)) | |
| except Exception as e: | |
| return { | |
| "verdict": "SKIP", | |
| "issues": [], | |
| "summary": f"Z3 verification exception (non-blocking): {e}", | |
| } | |
| if all_issues: | |
| return { | |
| "verdict": "UNSAFE", | |
| "issues": all_issues, | |
| "summary": f"Z3 found {len(all_issues)} formal issue(s): {all_issues[0]}", | |
| } | |
| return { | |
| "verdict": "SAFE", | |
| "issues": [], | |
| "summary": "Z3 found no definitive integer/bounds violations in added lines", | |
| } | |