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
Raw
History Blame Contribute Delete
8.2 kB
"""
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")
@dataclass
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
@dataclass
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()