""" Rhodawk AI — Adversarial Reviewer (Consensus Edition) ===================================================== Upgraded from sequential model-chain to concurrent 3-model consensus. Requires 2/3 majority to APPROVE or REJECT a diff. This eliminates single-model veto false-positives and false-negatives. Architecture: Before: Qwen → Gemma → Mistral (sequential, first success wins) After: Qwen ∥ Gemma ∥ Mistral (concurrent) → majority vote threshold=0.67 """ import concurrent.futures import hashlib import json import os import time import requests from requests.exceptions import HTTPError OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") ADVERSARY_MODEL_PRIMARY = os.getenv( "RHODAWK_ADVERSARY_MODEL", "deepseek/deepseek-r1:free" ) ADVERSARY_MODEL_SECONDARY = "meta-llama/llama-3.3-70b-instruct:free" ADVERSARY_MODEL_TERTIARY = "google/gemma-3-27b-it:free" _MODEL_CHAIN = [ ADVERSARY_MODEL_PRIMARY, ADVERSARY_MODEL_SECONDARY, ADVERSARY_MODEL_TERTIARY, ] CONSENSUS_THRESHOLD = float(os.getenv("RHODAWK_CONSENSUS_THRESHOLD", "0.67")) _RATE_LIMIT_WAIT = 20 ADVERSARY_SYSTEM_PROMPT = """You are a hostile senior security engineer and code quality enforcer. Your ONLY job is to find problems in AI-generated code fixes. Be adversarial. Be thorough. Be brutal. You are reviewing a diff produced by an AI to fix a failing test. Your job is to find: 1. SECURITY ISSUES: hardcoded credentials, injection risks, path traversal, insecure deserialization, dangerous imports (os.system, eval, exec), secrets in code, SSRF vectors 2. CORRECTNESS ISSUES: does the fix actually solve the root cause or just suppress the symptom? Does it handle edge cases? Will it break on different inputs? 3. REGRESSION RISKS: does this change break other functionality? Does it change public API signatures? Does it modify behavior for the passing cases? 4. CODE QUALITY: does this fix increase cyclomatic complexity significantly? Does it introduce dead code, duplicate logic, or anti-patterns? 5. SUPPLY CHAIN: does it add new dependencies? Are they trustworthy? Do they have known CVEs? Respond ONLY in this exact JSON format: { "verdict": "APPROVE" | "CONDITIONAL" | "REJECT", "confidence": 0.0-1.0, "critical_issues": ["issue1", "issue2"], "warnings": ["warning1", "warning2"], "summary": "one sentence verdict summary", "retry_guidance": "if REJECT: specific guidance for what the primary AI should do differently" } verdict rules: - REJECT if ANY critical_issues exist (security vulnerabilities, correctness failures that will cause runtime errors, regressions) - CONDITIONAL if only warnings exist (code quality, minor style, non-critical concerns) - APPROVE if no issues found """ def _call_openrouter(model: str, system: str, user: str, timeout: int = 60) -> dict: headers = { "Authorization": f"Bearer {OPENROUTER_API_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://rhodawk.ai", "X-Title": "Rhodawk AI Adversarial Reviewer", } payload = { "model": model.replace("openrouter/", ""), "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], "temperature": 0.1, "max_tokens": 1024, "response_format": {"type": "json_object"}, } resp = requests.post( "https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, timeout=timeout, ) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"] return json.loads(content) def _call_single_model(model: str, user_prompt: str) -> tuple[dict | None, str]: """Call one model; return (result_dict, model_name) or (None, model_name) on failure.""" try: result = _call_openrouter(model, ADVERSARY_SYSTEM_PROMPT, user_prompt) return result, model except HTTPError as e: status = e.response.status_code if e.response is not None else 0 if status == 429: time.sleep(_RATE_LIMIT_WAIT) return None, model except Exception: return None, model def _call_concurrent_consensus(user_prompt: str) -> tuple[dict, str]: """ Run all models concurrently and compute majority verdict. Returns (merged_result, summary_of_models_used). Falls back to sequential chain if all concurrent calls fail. """ results: list[tuple[dict, str]] = [] with concurrent.futures.ThreadPoolExecutor(max_workers=len(_MODEL_CHAIN)) as executor: futures = { executor.submit(_call_single_model, model, user_prompt): model for model in _MODEL_CHAIN } for future in concurrent.futures.as_completed(futures, timeout=90): try: result, model = future.result() if result is not None: results.append((result, model)) except Exception: pass if not results: raise RuntimeError("All concurrent adversarial models failed.") verdicts = [r[0].get("verdict", "CONDITIONAL") for r in results] models_used = [r[1] for r in results] verdict_counts: dict[str, int] = {} for v in verdicts: verdict_counts[v] = verdict_counts.get(v, 0) + 1 n = len(verdicts) majority_verdict = max(verdict_counts, key=lambda v: verdict_counts[v]) majority_fraction = verdict_counts[majority_verdict] / n if n > 0 else 0 if majority_fraction < CONSENSUS_THRESHOLD: majority_verdict = "CONDITIONAL" merged_critical: list[str] = [] merged_warnings: list[str] = [] merged_guidance: list[str] = [] merged_confidence: list[float] = [] merged_summary: list[str] = [] for r, _ in results: merged_critical.extend(r.get("critical_issues", [])) merged_warnings.extend(r.get("warnings", [])) if r.get("retry_guidance"): merged_guidance.append(r["retry_guidance"]) merged_confidence.append(float(r.get("confidence", 0.5))) if r.get("summary"): merged_summary.append(r["summary"]) unique_critical = list(dict.fromkeys(merged_critical)) unique_warnings = list(dict.fromkeys(merged_warnings)) avg_confidence = sum(merged_confidence) / len(merged_confidence) if merged_confidence else 0.5 consensus_summary = ( f"[Consensus {majority_fraction:.0%} on {majority_verdict}] " + "; ".join(merged_summary[:2]) ) merged_result = { "verdict": majority_verdict, "confidence": round(avg_confidence, 3), "critical_issues": unique_critical, "warnings": unique_warnings, "summary": consensus_summary, "retry_guidance": " | ".join(merged_guidance[:2]), "consensus_votes": verdict_counts, "consensus_fraction": round(majority_fraction, 3), } return merged_result, f"consensus({','.join(m.split('/')[-1] for m in models_used)})" def _call_with_model_chain(user_prompt: str) -> tuple[dict, str]: """ Legacy sequential fallback — used only if concurrent call is explicitly disabled. """ last_error = None for model in _MODEL_CHAIN: try: result = _call_openrouter(model, ADVERSARY_SYSTEM_PROMPT, user_prompt) return result, model except HTTPError as e: status = e.response.status_code if e.response is not None else 0 if status == 429: time.sleep(_RATE_LIMIT_WAIT) last_error = e continue except Exception as e: last_error = e continue raise RuntimeError(f"All models in chain failed. Last error: {last_error}") def run_adversarial_review( diff_text: str, test_path: str, original_failure: str, repo: str, ) -> dict: """ Run the concurrent adversarial consensus review on an AI-generated diff. Returns a dict with: verdict: "APPROVE" | "CONDITIONAL" | "REJECT" confidence: float (avg across models) critical_issues: list[str] warnings: list[str] summary: str retry_guidance: str model_used: str review_hash: str timestamp: str consensus_votes: dict (NEW — breakdown by model verdict) consensus_fraction: float (NEW — majority fraction) """ if not OPENROUTER_API_KEY: return { "verdict": "APPROVE", "confidence": 0.5, "critical_issues": [], "warnings": ["Adversarial review skipped — OPENROUTER_API_KEY not set"], "summary": "Review skipped", "retry_guidance": "", "model_used": "none", "review_hash": "skipped", "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "consensus_votes": {}, "consensus_fraction": 0.0, } user_prompt = ( f"REPOSITORY: {repo}\n" f"FAILING TEST: {test_path}\n\n" f"ORIGINAL FAILURE OUTPUT:\n```\n{original_failure[:1500]}\n```\n\n" f"AI-GENERATED DIFF TO REVIEW:\n```diff\n{diff_text[:3000]}\n```\n\n" f"Find every problem with this diff. Be adversarial." ) use_concurrent = os.getenv("RHODAWK_ADVERSARY_SEQUENTIAL", "false").lower() != "true" try: if use_concurrent: result, model_used = _call_concurrent_consensus(user_prompt) else: result, model_used = _call_with_model_chain(user_prompt) except Exception as e: return { "verdict": "CONDITIONAL", "confidence": 0.5, "critical_issues": [], "warnings": [f"Adversarial review failed after trying all models: {e}"], "summary": "Review unavailable — proceeding with SAST gate only", "retry_guidance": "", "model_used": "failed", "review_hash": "failed", "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "consensus_votes": {}, "consensus_fraction": 0.0, } review_input = f"{diff_text}{original_failure}" review_hash = hashlib.sha256(review_input.encode()).hexdigest()[:16] return { "verdict": result.get("verdict", "CONDITIONAL"), "confidence": result.get("confidence", 0.5), "critical_issues": result.get("critical_issues", []), "warnings": result.get("warnings", []), "summary": result.get("summary", ""), "retry_guidance": result.get("retry_guidance", ""), "model_used": model_used, "review_hash": review_hash, "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "consensus_votes": result.get("consensus_votes", {}), "consensus_fraction": result.get("consensus_fraction", 0.0), }