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 | """ | |
| ARCHITECT — autonomous "night-mode" loop (§5.2 of the Masterplan). | |
| Phase schedule (operator-local time): | |
| 18:00 → scope ingestion (HackerOne / Bugcrowd / Intigriti) | |
| 18:30 → reconnaissance fan-out per top target | |
| 20:00 → vulnerability hunt — 5 specialist agents in parallel | |
| 04:00 → report drafting | |
| 08:00 → operator review handoff (Telegram nudge) | |
| The scheduler is opt-in (``ARCHITECT_NIGHTMODE=1``). It never executes any | |
| submission action; the operator remains the final gate on every report. | |
| """ | |
| from __future__ import annotations | |
| import datetime as _dt | |
| import logging | |
| import os | |
| import threading | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Any, Callable | |
| from . import embodied_bridge | |
| LOG = logging.getLogger("architect.nightmode") | |
| class PhaseResult: | |
| name: str | |
| started_at: str | |
| completed_at: str | None = None | |
| targets: list[str] = field(default_factory=list) | |
| findings: list[dict] = field(default_factory=list) | |
| error: str | None = None | |
| def _now() -> str: | |
| return _dt.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") | |
| # ── Phase callables (overridable for tests) ───────────────────────────────── | |
| def _phase_scope_ingest() -> list[str]: | |
| """Pull active programs from every linked bounty platform.""" | |
| targets: list[str] = [] | |
| try: | |
| from mythos.mcp.scope_parser_mcp import server as scope | |
| out = scope.call("list_active_programs", {}) | |
| for p in (out or {}).get("programs", []): | |
| for asset in p.get("in_scope_assets", []): | |
| targets.append(asset) | |
| except Exception as exc: # noqa: BLE001 | |
| LOG.warning("scope ingest failed: %s", exc) | |
| return targets[: int(os.getenv("ARCHITECT_NIGHTMODE_MAX_TARGETS", "10"))] | |
| def _phase_recon(target: str) -> dict[str, Any]: | |
| out: dict[str, Any] = {"target": target} | |
| try: | |
| from mythos.mcp.subdomain_enum_mcp import server as subs | |
| out["subdomains"] = subs.call("enumerate", {"target": target}).get("subdomains", []) | |
| except Exception as exc: # noqa: BLE001 | |
| out["subdomains_error"] = str(exc) | |
| try: | |
| from mythos.mcp.httpx_probe_mcp import server as probe | |
| out["live_hosts"] = probe.call("probe", {"hosts": out.get("subdomains", [])}).get("live", []) | |
| except Exception as exc: # noqa: BLE001 | |
| out["probe_error"] = str(exc) | |
| try: | |
| from mythos.mcp.wayback_mcp import server as wb | |
| out["historical_urls"] = wb.call("snapshots", {"domain": target}).get("urls", []) | |
| except Exception as exc: # noqa: BLE001 | |
| out["wayback_error"] = str(exc) | |
| return out | |
| def _phase_hunt(target: str, recon: dict[str, Any]) -> list[dict]: | |
| """Run the 5 specialist agents (auth, server-side, logic, infra, api).""" | |
| findings: list[dict] = [] | |
| # Optional: import the multi-agent Mythos orchestrator for this target. | |
| try: | |
| from mythos import build_default_orchestrator | |
| orch = build_default_orchestrator(max_iterations=2) | |
| dossier = orch.run_campaign({ | |
| "repo": target, "repo_path": "/tmp/none", | |
| "languages": ["http"], "frameworks": [], "dependencies": [], | |
| "harness_dir": f"/tmp/architect/{target}", | |
| "extra_context": {"recon": recon}, | |
| }) | |
| for it in dossier.get("iterations", []): | |
| for h in it.get("findings", []): | |
| findings.append(h) | |
| except Exception as exc: # noqa: BLE001 | |
| LOG.warning("hunt failed for %s: %s", target, exc) | |
| return findings | |
| def _phase_report(findings: list[dict]) -> list[dict]: | |
| """Filter to ACTS ≥ 0.72 and format for human review.""" | |
| out: list[dict] = [] | |
| for h in findings: | |
| if h.get("acts_score", 0.0) < float(os.getenv("ARCHITECT_ACTS_GATE", "0.72")): | |
| continue | |
| out.append({ | |
| "title": h.get("title", "(untitled)"), | |
| "severity": h.get("severity", "P3"), | |
| "cwe": h.get("cwe", "?"), | |
| "summary": h.get("description", "")[:280], | |
| "acts_score": h.get("acts_score", 0.0), | |
| }) | |
| return out | |
| class NightRun: | |
| started_at: str = field(default_factory=_now) | |
| finished_at: str | None = None | |
| phases: list[PhaseResult] = field(default_factory=list) | |
| summary: dict[str, Any] = field(default_factory=dict) | |
| def run_one_cycle() -> NightRun: | |
| # W-009 FIX: coordinate with night_hunt_orchestrator.py so the two | |
| # autonomous bug-bounty loops never run overlapping cycles. | |
| import sys, os as _os | |
| sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))) | |
| from night_hunt_lock import try_acquire_night_hunt, release_night_hunt, is_locked | |
| holder = "architect-nightmode" | |
| if not try_acquire_night_hunt(holder): | |
| _, other, held_for = is_locked() | |
| LOG.warning( | |
| "ARCHITECT night-mode cycle skipped — another hunt loop (%s) " | |
| "holds the lock for %.0fs already. Set RHODAWK_NIGHT_HUNT_LOCK=false " | |
| "to disable this guard.", other, held_for, | |
| ) | |
| embodied_bridge.emit_status( | |
| f"Night-mode cycle skipped — lock held by {other}", "warn", | |
| ) | |
| skip = NightRun() | |
| skip.summary = {"skipped": True, "lock_holder": other} | |
| skip.finished_at = _now() | |
| return skip | |
| try: | |
| return _run_one_cycle_inner() | |
| finally: | |
| release_night_hunt(holder) | |
| def _run_one_cycle_inner() -> NightRun: | |
| run = NightRun() | |
| embodied_bridge.emit_status("Night-mode cycle started", "info") | |
| p1 = PhaseResult(name="scope_ingest", started_at=_now()) | |
| try: | |
| p1.targets = _phase_scope_ingest() | |
| except Exception as exc: # noqa: BLE001 | |
| p1.error = str(exc) | |
| p1.completed_at = _now() | |
| run.phases.append(p1) | |
| all_findings: list[dict] = [] | |
| for tgt in p1.targets: | |
| pr = PhaseResult(name=f"recon[{tgt}]", started_at=_now()) | |
| recon = _phase_recon(tgt) | |
| pr.completed_at = _now() | |
| run.phases.append(pr) | |
| ph = PhaseResult(name=f"hunt[{tgt}]", started_at=_now()) | |
| try: | |
| ph.findings = _phase_hunt(tgt, recon) | |
| all_findings.extend(ph.findings) | |
| except Exception as exc: # noqa: BLE001 | |
| ph.error = str(exc) | |
| ph.completed_at = _now() | |
| run.phases.append(ph) | |
| rep = PhaseResult(name="report", started_at=_now()) | |
| rep.findings = _phase_report(all_findings) | |
| rep.completed_at = _now() | |
| run.phases.append(rep) | |
| run.summary = { | |
| "targets": len(p1.targets), | |
| "raw_findings": len(all_findings), | |
| "qualified_findings": len(rep.findings), | |
| } | |
| run.finished_at = _now() | |
| embodied_bridge.emit_status( | |
| f"Night-mode complete — {run.summary['qualified_findings']} reviewable finding(s) " | |
| f"across {run.summary['targets']} target(s)", "info") | |
| return run | |
| def _next_run_time(hour: int) -> float: | |
| now = _dt.datetime.now() | |
| target = now.replace(hour=hour, minute=0, second=0, microsecond=0) | |
| if target <= now: | |
| target += _dt.timedelta(days=1) | |
| return target.timestamp() | |
| def schedule_loop(start_hour: int | None = None) -> None: | |
| """Daemon loop: runs ``run_one_cycle`` once per day at start_hour:00 local.""" | |
| if not os.getenv("ARCHITECT_NIGHTMODE", "0").lower() in ("1", "true", "yes", "on"): | |
| LOG.info("ARCHITECT night-mode disabled (set ARCHITECT_NIGHTMODE=1)") | |
| return | |
| hour = int(start_hour if start_hour is not None | |
| else os.getenv("ARCHITECT_NIGHTMODE_HOUR", "18")) | |
| LOG.info("ARCHITECT night-mode scheduler armed for %02d:00 daily", hour) | |
| while True: | |
| wake = _next_run_time(hour) | |
| time.sleep(max(1.0, wake - time.time())) | |
| try: | |
| run_one_cycle() | |
| except Exception as exc: # noqa: BLE001 | |
| LOG.exception("Night-mode cycle crashed: %s", exc) | |
| embodied_bridge.emit_status(f"Night-mode crashed: {exc}", "error") | |
| def start_in_background() -> None: | |
| threading.Thread(target=schedule_loop, daemon=True, name="architect-nightmode").start() | |