Rhodawk Agent commited on
Commit ·
6ed9a00
1
Parent(s): 58fe8fa
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.
Browse files- Makefile +25 -0
- Rhodawk_AI_Pitch_Deck_2026.pptx +0 -3
- app.py +46 -0
- architect/nightmode.py +29 -0
- formal_verifier.py +21 -1
- hermes_orchestrator.py +42 -2
- lora_scheduler.py +23 -0
- mcp_config.json → mcp_config.ARCHIVE.json +37 -27
- night_hunt_lock.py +95 -0
- night_hunt_orchestrator.py +38 -12
- repo_harvester.py +9 -1
- scripts/generate_stubs.sh +52 -0
- training_store.py +28 -1
- webhook_server.py +14 -0
Makefile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rhodawk AI — Local Developer Makefile
|
| 2 |
+
# Resolves W-001: provides a one-shot target to generate gRPC stubs locally
|
| 3 |
+
# so the Python brain can run without requiring a full Docker build.
|
| 4 |
+
|
| 5 |
+
.PHONY: help stubs install dev clean
|
| 6 |
+
|
| 7 |
+
help:
|
| 8 |
+
@echo "Rhodawk AI — local developer targets"
|
| 9 |
+
@echo " make stubs Generate openclaude_pb2*.py gRPC stubs (W-001)"
|
| 10 |
+
@echo " make install pip install -r requirements.txt + grpcio-tools"
|
| 11 |
+
@echo " make dev Run app.py locally (requires stubs + env vars)"
|
| 12 |
+
@echo " make clean Remove generated gRPC stubs"
|
| 13 |
+
|
| 14 |
+
stubs:
|
| 15 |
+
bash scripts/generate_stubs.sh
|
| 16 |
+
|
| 17 |
+
install:
|
| 18 |
+
pip install -r requirements.txt
|
| 19 |
+
pip install grpcio-tools
|
| 20 |
+
|
| 21 |
+
dev: stubs
|
| 22 |
+
python -u app.py
|
| 23 |
+
|
| 24 |
+
clean:
|
| 25 |
+
rm -f openclaude_grpc/openclaude_pb2.py openclaude_grpc/openclaude_pb2_grpc.py
|
Rhodawk_AI_Pitch_Deck_2026.pptx
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:952a8e738f0ccbba104c32d018782ac0c2fce30e6833796a5bed5f6588c2d4d4
|
| 3 |
-
size 1639252
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1969,6 +1969,52 @@ with gr.Blocks(title="Rhodawk AI — Code Review Monster", theme=THEME) as demo:
|
|
| 1969 |
# ── TAB 1: LIVE OPERATIONS ──────────────────────────────
|
| 1970 |
with gr.Tab("⚡ Live Operations"):
|
| 1971 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1972 |
# ── STATUS BAR ──────────────────────────────────────
|
| 1973 |
with gr.Row():
|
| 1974 |
stat_status = gr.Textbox(label="System Status", interactive=False, scale=3)
|
|
|
|
| 1969 |
# ── TAB 1: LIVE OPERATIONS ──────────────────────────────
|
| 1970 |
with gr.Tab("⚡ Live Operations"):
|
| 1971 |
|
| 1972 |
+
# ── W-005 + W-012 FIX: SYSTEM STATUS BANNER ─────────
|
| 1973 |
+
# Explicitly surface the ENABLED/DISABLED state of every major
|
| 1974 |
+
# opt-in subsystem (Z3, Auto-Merge, Mythos, ARCHITECT, LoRA) so
|
| 1975 |
+
# operators are never misled into thinking a tab implies a live
|
| 1976 |
+
# capability. Renders once at UI build time from env vars.
|
| 1977 |
+
def _flag(name: str, default: str = "false") -> str:
|
| 1978 |
+
v = os.getenv(name, default).lower().strip()
|
| 1979 |
+
on = v in ("1", "true", "yes", "on")
|
| 1980 |
+
return ("✅ ENABLED" if on else "⚠️ DISABLED")
|
| 1981 |
+
|
| 1982 |
+
_z3_state = _flag("RHODAWK_Z3_ENABLED", "true") # W-004 default flipped
|
| 1983 |
+
_auto_merge_state = _flag("RHODAWK_AUTO_MERGE", "false")
|
| 1984 |
+
_mythos_state = _flag("RHODAWK_MYTHOS", "0")
|
| 1985 |
+
_architect_state = _flag("ARCHITECT_NIGHTMODE", "0")
|
| 1986 |
+
_lora_state = _flag("RHODAWK_LORA_ENABLED", "false")
|
| 1987 |
+
_hermes_provider = os.getenv("HERMES_PROVIDER", "auto") # W-008
|
| 1988 |
+
_night_lock_state = _flag("RHODAWK_NIGHT_HUNT_LOCK", "true") # W-009
|
| 1989 |
+
|
| 1990 |
+
gr.HTML(f"""
|
| 1991 |
+
<div style="margin:8px 0 16px 0; padding:14px 18px;
|
| 1992 |
+
background:#0a0f1f; border:1px solid #2d3a5a;
|
| 1993 |
+
border-left:4px solid #7c3aed; border-radius:10px;
|
| 1994 |
+
font-family:ui-monospace,Menlo,monospace; font-size:0.82rem;">
|
| 1995 |
+
<div style="font-weight:700; color:#a78bfa; letter-spacing:0.06em;
|
| 1996 |
+
text-transform:uppercase; margin-bottom:8px;">
|
| 1997 |
+
System Status — Feature Gates
|
| 1998 |
+
</div>
|
| 1999 |
+
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
|
| 2000 |
+
gap:6px 18px; color:#cbd5e1;">
|
| 2001 |
+
<div>Z3 Formal Verification: <b>{_z3_state}</b></div>
|
| 2002 |
+
<div>Auto-Merge (Conviction Engine): <b>{_auto_merge_state}</b></div>
|
| 2003 |
+
<div>Mythos Multi-Agent: <b>{_mythos_state}</b></div>
|
| 2004 |
+
<div>ARCHITECT Night-Mode: <b>{_architect_state}</b></div>
|
| 2005 |
+
<div>LoRA Scheduler: <b>{_lora_state}</b></div>
|
| 2006 |
+
<div>Night-Hunt Mutex (W-009): <b>{_night_lock_state}</b></div>
|
| 2007 |
+
<div>Hermes Provider: <b>{_hermes_provider}</b></div>
|
| 2008 |
+
</div>
|
| 2009 |
+
<div style="margin-top:8px; color:#64748b; font-size:0.75rem;">
|
| 2010 |
+
Auto-Merge is OFF by default for safety. Enable with
|
| 2011 |
+
<code style="color:#a78bfa;">RHODAWK_AUTO_MERGE=true</code> only after
|
| 2012 |
+
you trust the conviction engine on your repo. Disabled subsystems
|
| 2013 |
+
show their tab in the UI but do NOT execute at runtime.
|
| 2014 |
+
</div>
|
| 2015 |
+
</div>
|
| 2016 |
+
""")
|
| 2017 |
+
|
| 2018 |
# ── STATUS BAR ──────────────────────────────────────
|
| 2019 |
with gr.Row():
|
| 2020 |
stat_status = gr.Textbox(label="System Status", interactive=False, scale=3)
|
architect/nightmode.py
CHANGED
|
@@ -123,6 +123,35 @@ class NightRun:
|
|
| 123 |
|
| 124 |
|
| 125 |
def run_one_cycle() -> NightRun:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
run = NightRun()
|
| 127 |
embodied_bridge.emit_status("Night-mode cycle started", "info")
|
| 128 |
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
def run_one_cycle() -> NightRun:
|
| 126 |
+
# W-009 FIX: coordinate with night_hunt_orchestrator.py so the two
|
| 127 |
+
# autonomous bug-bounty loops never run overlapping cycles.
|
| 128 |
+
import sys, os as _os
|
| 129 |
+
sys.path.insert(0, _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))))
|
| 130 |
+
from night_hunt_lock import try_acquire_night_hunt, release_night_hunt, is_locked
|
| 131 |
+
|
| 132 |
+
holder = "architect-nightmode"
|
| 133 |
+
if not try_acquire_night_hunt(holder):
|
| 134 |
+
_, other, held_for = is_locked()
|
| 135 |
+
LOG.warning(
|
| 136 |
+
"ARCHITECT night-mode cycle skipped — another hunt loop (%s) "
|
| 137 |
+
"holds the lock for %.0fs already. Set RHODAWK_NIGHT_HUNT_LOCK=false "
|
| 138 |
+
"to disable this guard.", other, held_for,
|
| 139 |
+
)
|
| 140 |
+
embodied_bridge.emit_status(
|
| 141 |
+
f"Night-mode cycle skipped — lock held by {other}", "warn",
|
| 142 |
+
)
|
| 143 |
+
skip = NightRun()
|
| 144 |
+
skip.summary = {"skipped": True, "lock_holder": other}
|
| 145 |
+
skip.finished_at = _now()
|
| 146 |
+
return skip
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
return _run_one_cycle_inner()
|
| 150 |
+
finally:
|
| 151 |
+
release_night_hunt(holder)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _run_one_cycle_inner() -> NightRun:
|
| 155 |
run = NightRun()
|
| 156 |
embodied_bridge.emit_status("Night-mode cycle started", "info")
|
| 157 |
|
formal_verifier.py
CHANGED
|
@@ -21,7 +21,11 @@ Enable: RHODAWK_Z3_ENABLED=true
|
|
| 21 |
import os
|
| 22 |
import re
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
_IMPORT_OK = False
|
| 27 |
try:
|
|
@@ -30,6 +34,22 @@ try:
|
|
| 30 |
except ImportError:
|
| 31 |
pass
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def _extract_added_lines(diff_text: str) -> list[str]:
|
| 35 |
return [
|
|
|
|
| 21 |
import os
|
| 22 |
import re
|
| 23 |
|
| 24 |
+
# W-004 FIX: Z3 formal verification gate is now ON by default.
|
| 25 |
+
# z3-solver is already pinned in requirements.txt. Operators may explicitly
|
| 26 |
+
# disable it with RHODAWK_Z3_ENABLED=false. A loud startup warning is emitted
|
| 27 |
+
# whenever Z3 is skipped (either disabled or import failure).
|
| 28 |
+
Z3_ENABLED = os.getenv("RHODAWK_Z3_ENABLED", "true").lower() == "true"
|
| 29 |
|
| 30 |
_IMPORT_OK = False
|
| 31 |
try:
|
|
|
|
| 34 |
except ImportError:
|
| 35 |
pass
|
| 36 |
|
| 37 |
+
import sys as _sys
|
| 38 |
+
if not Z3_ENABLED:
|
| 39 |
+
print(
|
| 40 |
+
"[STARTUP WARNING] Z3 formal verification gate is DISABLED "
|
| 41 |
+
"(RHODAWK_Z3_ENABLED=false). Step 7b of the healing loop will "
|
| 42 |
+
"be skipped and no UNSAFE diffs will be blocked by Z3.",
|
| 43 |
+
file=_sys.stderr,
|
| 44 |
+
)
|
| 45 |
+
elif not _IMPORT_OK:
|
| 46 |
+
print(
|
| 47 |
+
"[STARTUP WARNING] Z3 formal verification gate is ENABLED but "
|
| 48 |
+
"z3-solver is not installed. Run: pip install z3-solver. "
|
| 49 |
+
"Step 7b of the healing loop will return SKIP.",
|
| 50 |
+
file=_sys.stderr,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
|
| 54 |
def _extract_added_lines(diff_text: str) -> list[str]:
|
| 55 |
return [
|
hermes_orchestrator.py
CHANGED
|
@@ -44,6 +44,19 @@ HERMES_MODEL = os.getenv("HERMES_MODEL", "deepseek/deepseek-r1:free")
|
|
| 44 |
HERMES_FAST_MODEL = os.getenv("HERMES_FAST_MODEL", "deepseek/deepseek-v3:free")
|
| 45 |
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
# ── DigitalOcean Serverless Inference (PRIMARY provider) ────────────────
|
| 48 |
# OpenAI-compatible REST API. We POST to /chat/completions just like
|
| 49 |
# OpenRouter; only the base URL, auth header, and model name differ.
|
|
@@ -467,11 +480,38 @@ def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120
|
|
| 467 |
2. On any non-recoverable failure or exhausted rate-limit retries,
|
| 468 |
fall back to OpenRouter (OPENROUTER_API_KEY).
|
| 469 |
3. If neither is configured, return a graceful no-op.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
"""
|
| 471 |
requested_model = model or HERMES_MODEL
|
| 472 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
providers: list[tuple[str, str, str, str, dict]] = []
|
| 474 |
-
|
|
|
|
|
|
|
| 475 |
do_model = (
|
| 476 |
_strip_provider_prefix(requested_model)
|
| 477 |
if requested_model.startswith(("openai/",))
|
|
@@ -480,7 +520,7 @@ def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120
|
|
| 480 |
providers.append(
|
| 481 |
("DigitalOcean", DO_INFERENCE_BASE, DO_INFERENCE_API_KEY, do_model, {})
|
| 482 |
)
|
| 483 |
-
if OPENROUTER_API_KEY:
|
| 484 |
or_model = _strip_provider_prefix(requested_model) if "/" in requested_model else requested_model
|
| 485 |
# OpenRouter expects models in `vendor/name` form — only re-add the
|
| 486 |
# prefix when the caller passed an `openai/...` (DO-shaped) string.
|
|
|
|
| 44 |
HERMES_FAST_MODEL = os.getenv("HERMES_FAST_MODEL", "deepseek/deepseek-v3:free")
|
| 45 |
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
|
| 46 |
|
| 47 |
+
# W-008 FIX: explicit provider routing flag so the operator can force Hermes
|
| 48 |
+
# through the OpenClaude gRPC daemon (which itself fails over DO → OpenRouter
|
| 49 |
+
# inside the daemon process). Without this flag, Hermes was bypassing the
|
| 50 |
+
# OpenClaude daemon entirely, breaking cost attribution and rate-limit
|
| 51 |
+
# budgeting.
|
| 52 |
+
#
|
| 53 |
+
# Allowed values:
|
| 54 |
+
# "auto" — try DO Inference REST then OpenRouter REST (legacy)
|
| 55 |
+
# "openclaude_grpc" — route through openclaude_grpc.client (DO daemon :50051)
|
| 56 |
+
# "do" — DO Inference REST only
|
| 57 |
+
# "openrouter" — OpenRouter REST only
|
| 58 |
+
HERMES_PROVIDER = os.getenv("HERMES_PROVIDER", "auto").lower().strip()
|
| 59 |
+
|
| 60 |
# ── DigitalOcean Serverless Inference (PRIMARY provider) ────────────────
|
| 61 |
# OpenAI-compatible REST API. We POST to /chat/completions just like
|
| 62 |
# OpenRouter; only the base URL, auth header, and model name differ.
|
|
|
|
| 480 |
2. On any non-recoverable failure or exhausted rate-limit retries,
|
| 481 |
fall back to OpenRouter (OPENROUTER_API_KEY).
|
| 482 |
3. If neither is configured, return a graceful no-op.
|
| 483 |
+
|
| 484 |
+
W-008 FIX: respect HERMES_PROVIDER env var. When set to
|
| 485 |
+
"openclaude_grpc" all calls are routed through the OpenClaude gRPC
|
| 486 |
+
daemon (DigitalOcean primary on :50051, OpenRouter fallback on :50052)
|
| 487 |
+
instead of bypassing the daemon with direct REST calls.
|
| 488 |
"""
|
| 489 |
requested_model = model or HERMES_MODEL
|
| 490 |
|
| 491 |
+
# W-008 FIX: openclaude_grpc routing path.
|
| 492 |
+
if HERMES_PROVIDER == "openclaude_grpc":
|
| 493 |
+
try:
|
| 494 |
+
from openclaude_grpc.client import OpenClaudeClient
|
| 495 |
+
hermes_log("LLM call → openclaude_grpc daemon (:50051)", "HERMES")
|
| 496 |
+
prompt_text = "\n\n".join(
|
| 497 |
+
f"[{m.get('role', 'user').upper()}] {m.get('content', '')}"
|
| 498 |
+
for m in messages
|
| 499 |
+
)
|
| 500 |
+
client = OpenClaudeClient(host="127.0.0.1", port=50051)
|
| 501 |
+
combined, exit_code = client.chat(prompt_text, timeout=timeout)
|
| 502 |
+
try:
|
| 503 |
+
return json.loads(combined)
|
| 504 |
+
except (json.JSONDecodeError, TypeError):
|
| 505 |
+
return {"done": exit_code == 0, "summary": combined}
|
| 506 |
+
except Exception as exc:
|
| 507 |
+
hermes_log(f"openclaude_grpc routing failed: {exc} — falling back to REST",
|
| 508 |
+
"WARN")
|
| 509 |
+
# Fall through to REST providers below.
|
| 510 |
+
|
| 511 |
providers: list[tuple[str, str, str, str, dict]] = []
|
| 512 |
+
do_allowed = HERMES_PROVIDER in ("auto", "do", "openclaude_grpc")
|
| 513 |
+
or_allowed = HERMES_PROVIDER in ("auto", "openrouter", "openclaude_grpc")
|
| 514 |
+
if do_allowed and DO_INFERENCE_API_KEY:
|
| 515 |
do_model = (
|
| 516 |
_strip_provider_prefix(requested_model)
|
| 517 |
if requested_model.startswith(("openai/",))
|
|
|
|
| 520 |
providers.append(
|
| 521 |
("DigitalOcean", DO_INFERENCE_BASE, DO_INFERENCE_API_KEY, do_model, {})
|
| 522 |
)
|
| 523 |
+
if or_allowed and OPENROUTER_API_KEY:
|
| 524 |
or_model = _strip_provider_prefix(requested_model) if "/" in requested_model else requested_model
|
| 525 |
# OpenRouter expects models in `vendor/name` form — only re-add the
|
| 526 |
# prefix when the caller passed an `openai/...` (DO-shaped) string.
|
lora_scheduler.py
CHANGED
|
@@ -164,6 +164,29 @@ def run_training_export() -> dict:
|
|
| 164 |
for ex in examples:
|
| 165 |
f.write(json.dumps(ex) + "\n")
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
state = _load_state()
|
| 168 |
try:
|
| 169 |
with sqlite3.connect(DB_PATH) as conn:
|
|
|
|
| 164 |
for ex in examples:
|
| 165 |
f.write(json.dumps(ex) + "\n")
|
| 166 |
|
| 167 |
+
# W-011 FIX: notify operators (Telegram + Slack) on every successful
|
| 168 |
+
# LoRA JSONL export so they know fresh training data is ready, instead of
|
| 169 |
+
# silently accumulating files in /data/lora_exports/ with no consumer.
|
| 170 |
+
try:
|
| 171 |
+
import notifier as _notifier # type: ignore
|
| 172 |
+
msg = (
|
| 173 |
+
f"📦 *Rhodawk LoRA Export*\n"
|
| 174 |
+
f"• Samples: `{len(examples)}`\n"
|
| 175 |
+
f"• Path: `{out_path}`\n"
|
| 176 |
+
f"• Timestamp: `{timestamp}`\n"
|
| 177 |
+
f"Ready for HF PEFT/TRL/AutoTrain consumption."
|
| 178 |
+
)
|
| 179 |
+
if hasattr(_notifier, "notify"):
|
| 180 |
+
_notifier.notify(msg) # type: ignore[attr-defined]
|
| 181 |
+
elif hasattr(_notifier, "send_telegram"):
|
| 182 |
+
_notifier.send_telegram(msg) # type: ignore[attr-defined]
|
| 183 |
+
if hasattr(_notifier, "send_slack"):
|
| 184 |
+
_notifier.send_slack(msg) # type: ignore[attr-defined]
|
| 185 |
+
except Exception as _exc:
|
| 186 |
+
# Notifications are best-effort; never fail the export over a
|
| 187 |
+
# missing Telegram/Slack credential.
|
| 188 |
+
print(f"[lora_scheduler] notification dispatch skipped: {_exc}")
|
| 189 |
+
|
| 190 |
state = _load_state()
|
| 191 |
try:
|
| 192 |
with sqlite3.connect(DB_PATH) as conn:
|
mcp_config.json → mcp_config.ARCHIVE.json
RENAMED
|
@@ -1,10 +1,20 @@
|
|
| 1 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"_comment": [
|
| 3 |
-
"TEMPLATE ONLY
|
| 4 |
"Actual runtime config is written to /tmp/mcp_runtime.json at startup.",
|
| 5 |
-
"Secrets injected from HF Space env vars
|
| 6 |
"FETCH_ALLOWED_DOMAINS prevents SSRF against internal services.",
|
| 7 |
-
"Rhodawk AI v6.0
|
| 8 |
],
|
| 9 |
"mcpServers": {
|
| 10 |
"fetch-docs": {
|
|
@@ -45,7 +55,7 @@
|
|
| 45 |
"-y",
|
| 46 |
"@modelcontextprotocol/server-memory"
|
| 47 |
],
|
| 48 |
-
"description": "Persistent knowledge graph
|
| 49 |
},
|
| 50 |
"sequential-thinking": {
|
| 51 |
"command": "npx",
|
|
@@ -91,7 +101,7 @@
|
|
| 91 |
"--db-path",
|
| 92 |
"/data/rhodawk_findings.db"
|
| 93 |
],
|
| 94 |
-
"description": "Local findings store
|
| 95 |
},
|
| 96 |
"nuclei-scanner": {
|
| 97 |
"command": "uvx",
|
|
@@ -100,7 +110,7 @@
|
|
| 100 |
"--allow-commands",
|
| 101 |
"nuclei,nuclei-templates"
|
| 102 |
],
|
| 103 |
-
"description": "Nuclei template-based vulnerability scanner
|
| 104 |
"env": {
|
| 105 |
"NUCLEI_TEMPLATES_PATH": "/data/nuclei-templates",
|
| 106 |
"NUCLEI_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
|
@@ -113,7 +123,7 @@
|
|
| 113 |
"--allow-commands",
|
| 114 |
"semgrep"
|
| 115 |
],
|
| 116 |
-
"description": "Semgrep SAST
|
| 117 |
"env": {
|
| 118 |
"SEMGREP_APP_TOKEN": "__INJECTED_BY_APP_AT_RUNTIME__"
|
| 119 |
}
|
|
@@ -125,7 +135,7 @@
|
|
| 125 |
"--allow-commands",
|
| 126 |
"trufflehog"
|
| 127 |
],
|
| 128 |
-
"description": "TruffleHog v3
|
| 129 |
},
|
| 130 |
"bandit-sast": {
|
| 131 |
"command": "uvx",
|
|
@@ -134,7 +144,7 @@
|
|
| 134 |
"--allow-commands",
|
| 135 |
"bandit"
|
| 136 |
],
|
| 137 |
-
"description": "Bandit Python SAST
|
| 138 |
},
|
| 139 |
"pip-audit-sca": {
|
| 140 |
"command": "uvx",
|
|
@@ -143,7 +153,7 @@
|
|
| 143 |
"--allow-commands",
|
| 144 |
"pip-audit,pip"
|
| 145 |
],
|
| 146 |
-
"description": "pip-audit SCA
|
| 147 |
},
|
| 148 |
"osv-scanner": {
|
| 149 |
"command": "uvx",
|
|
@@ -152,7 +162,7 @@
|
|
| 152 |
"--allow-commands",
|
| 153 |
"osv-scanner"
|
| 154 |
],
|
| 155 |
-
"description": "OSV Scanner
|
| 156 |
},
|
| 157 |
"z3-formal-verifier": {
|
| 158 |
"command": "uvx",
|
|
@@ -161,7 +171,7 @@
|
|
| 161 |
"--allow-commands",
|
| 162 |
"python3"
|
| 163 |
],
|
| 164 |
-
"description": "Z3 SMT solver
|
| 165 |
},
|
| 166 |
"hypothesis-fuzzer": {
|
| 167 |
"command": "uvx",
|
|
@@ -170,7 +180,7 @@
|
|
| 170 |
"--allow-commands",
|
| 171 |
"python3,pytest,hypothesis"
|
| 172 |
],
|
| 173 |
-
"description": "Hypothesis PBT fuzzer
|
| 174 |
},
|
| 175 |
"atheris-fuzzer": {
|
| 176 |
"command": "uvx",
|
|
@@ -179,7 +189,7 @@
|
|
| 179 |
"--allow-commands",
|
| 180 |
"python3,atheris"
|
| 181 |
],
|
| 182 |
-
"description": "Atheris coverage-guided fuzzer
|
| 183 |
},
|
| 184 |
"angr-symbolic": {
|
| 185 |
"command": "uvx",
|
|
@@ -188,7 +198,7 @@
|
|
| 188 |
"--allow-commands",
|
| 189 |
"python3"
|
| 190 |
],
|
| 191 |
-
"description": "angr symbolic execution
|
| 192 |
},
|
| 193 |
"radon-complexity": {
|
| 194 |
"command": "uvx",
|
|
@@ -197,7 +207,7 @@
|
|
| 197 |
"--allow-commands",
|
| 198 |
"radon"
|
| 199 |
],
|
| 200 |
-
"description": "Radon AST complexity analysis
|
| 201 |
},
|
| 202 |
"ruff-linter": {
|
| 203 |
"command": "uvx",
|
|
@@ -206,7 +216,7 @@
|
|
| 206 |
"--allow-commands",
|
| 207 |
"ruff"
|
| 208 |
],
|
| 209 |
-
"description": "Ruff ultra-fast Python linter
|
| 210 |
},
|
| 211 |
"aider-patcher": {
|
| 212 |
"command": "uvx",
|
|
@@ -215,7 +225,7 @@
|
|
| 215 |
"--allow-commands",
|
| 216 |
"aider"
|
| 217 |
],
|
| 218 |
-
"description": "Aider AI code editor
|
| 219 |
"env": {
|
| 220 |
"OPENROUTER_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
| 221 |
}
|
|
@@ -225,7 +235,7 @@
|
|
| 225 |
"args": [
|
| 226 |
"mcp-server-fetch"
|
| 227 |
],
|
| 228 |
-
"description": "NVD/NIST CVE API
|
| 229 |
"env": {
|
| 230 |
"FETCH_ALLOWED_DOMAINS": "nvd.nist.gov,cve.org,cve.mitre.org,www.cvedetails.com,vulners.com,osv.dev,opencve.io",
|
| 231 |
"NVD_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
|
@@ -236,7 +246,7 @@
|
|
| 236 |
"args": [
|
| 237 |
"mcp-server-fetch"
|
| 238 |
],
|
| 239 |
-
"description": "Bug bounty platform APIs
|
| 240 |
"env": {
|
| 241 |
"FETCH_ALLOWED_DOMAINS": "api.hackerone.com,api.bugcrowd.com,api.intigriti.com,api.yeswehack.com,api.github.com",
|
| 242 |
"HACKERONE_API_TOKEN": "__INJECTED_BY_APP_AT_RUNTIME__",
|
|
@@ -248,7 +258,7 @@
|
|
| 248 |
"args": [
|
| 249 |
"mcp-server-fetch"
|
| 250 |
],
|
| 251 |
-
"description": "Supply chain security
|
| 252 |
"env": {
|
| 253 |
"FETCH_ALLOWED_DOMAINS": "pypi.org,api.pypi.org,registry.npmjs.org,crates.io,deps.dev,socket.dev,api.socket.dev"
|
| 254 |
}
|
|
@@ -267,7 +277,7 @@
|
|
| 267 |
"-m",
|
| 268 |
"mythos.mcp.static_analysis_mcp"
|
| 269 |
],
|
| 270 |
-
"description": "Mythos: Tree-sitter CPG, Joern, CodeQL, Semgrep
|
| 271 |
},
|
| 272 |
"dynamic-analysis-mcp": {
|
| 273 |
"command": "python",
|
|
@@ -275,7 +285,7 @@
|
|
| 275 |
"-m",
|
| 276 |
"mythos.mcp.dynamic_analysis_mcp"
|
| 277 |
],
|
| 278 |
-
"description": "Mythos: AFL++, KLEE, QEMU, Frida, GDB
|
| 279 |
},
|
| 280 |
"exploit-generation-mcp": {
|
| 281 |
"command": "python",
|
|
@@ -283,7 +293,7 @@
|
|
| 283 |
"-m",
|
| 284 |
"mythos.mcp.exploit_generation_mcp"
|
| 285 |
],
|
| 286 |
-
"description": "Mythos: Pwntools, ROPGadget, heap kit, privesc KB
|
| 287 |
},
|
| 288 |
"vulnerability-database-mcp": {
|
| 289 |
"command": "python",
|
|
@@ -396,7 +406,7 @@
|
|
| 396 |
"-m",
|
| 397 |
"mythos.mcp.skill_selector_mcp"
|
| 398 |
],
|
| 399 |
-
"description": "Rhodawk: semantic skill selection (MiniLM)
|
| 400 |
},
|
| 401 |
"trufflehog-deep-mcp": {
|
| 402 |
"command": "uvx",
|
|
@@ -414,7 +424,7 @@
|
|
| 414 |
"--allow-commands",
|
| 415 |
"gitleaks,git"
|
| 416 |
],
|
| 417 |
-
"description": "Gitleaks
|
| 418 |
},
|
| 419 |
"semgrep-pro-patterns-mcp": {
|
| 420 |
"command": "uvx",
|
|
@@ -496,4 +506,4 @@
|
|
| 496 |
"description": "JS prototype-pollution sink scanner (AST-style grep)"
|
| 497 |
}
|
| 498 |
}
|
| 499 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"_W003_ARCHIVE_NOTICE": [
|
| 3 |
+
"This file is ARCHIVED and NOT used at runtime.",
|
| 4 |
+
"The runtime MCP config is generated by write_mcp_config() in app.py",
|
| 5 |
+
"and written to /tmp/mcp_runtime.json at the start of every audit.",
|
| 6 |
+
"Removed entries (kept here for historical reference only):",
|
| 7 |
+
" - aider-patcher (replaced by OpenClaude)",
|
| 8 |
+
" - postgres-intelligence (npm package 404)",
|
| 9 |
+
" - atheris-fuzzer (requires Clang+libFuzzer, unavailable on HF Spaces)",
|
| 10 |
+
"Resolves W-003 (HIGH) from FOUNDER_PLAYBOOK.md section 10."
|
| 11 |
+
],
|
| 12 |
"_comment": [
|
| 13 |
+
"TEMPLATE ONLY \u2014 contains NO secrets.",
|
| 14 |
"Actual runtime config is written to /tmp/mcp_runtime.json at startup.",
|
| 15 |
+
"Secrets injected from HF Space env vars \u2014 never committed.",
|
| 16 |
"FETCH_ALLOWED_DOMAINS prevents SSRF against internal services.",
|
| 17 |
+
"Rhodawk AI v6.0 \u2014 37 cybersecurity MCP server suite (12 new in v6 per Masterplan \u00a78)."
|
| 18 |
],
|
| 19 |
"mcpServers": {
|
| 20 |
"fetch-docs": {
|
|
|
|
| 55 |
"-y",
|
| 56 |
"@modelcontextprotocol/server-memory"
|
| 57 |
],
|
| 58 |
+
"description": "Persistent knowledge graph \u2014 stores exploit chains, CWE patterns, and cross-session vulnerability memory"
|
| 59 |
},
|
| 60 |
"sequential-thinking": {
|
| 61 |
"command": "npx",
|
|
|
|
| 101 |
"--db-path",
|
| 102 |
"/data/rhodawk_findings.db"
|
| 103 |
],
|
| 104 |
+
"description": "Local findings store \u2014 fast queries on vulnerability metadata, CVSS scores, and bounty estimates"
|
| 105 |
},
|
| 106 |
"nuclei-scanner": {
|
| 107 |
"command": "uvx",
|
|
|
|
| 110 |
"--allow-commands",
|
| 111 |
"nuclei,nuclei-templates"
|
| 112 |
],
|
| 113 |
+
"description": "Nuclei template-based vulnerability scanner \u2014 DAST, CVE detection, misconfig scanning",
|
| 114 |
"env": {
|
| 115 |
"NUCLEI_TEMPLATES_PATH": "/data/nuclei-templates",
|
| 116 |
"NUCLEI_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
|
|
|
| 123 |
"--allow-commands",
|
| 124 |
"semgrep"
|
| 125 |
],
|
| 126 |
+
"description": "Semgrep SAST \u2014 taint analysis, CWE pattern matching, secrets detection across 30+ languages",
|
| 127 |
"env": {
|
| 128 |
"SEMGREP_APP_TOKEN": "__INJECTED_BY_APP_AT_RUNTIME__"
|
| 129 |
}
|
|
|
|
| 135 |
"--allow-commands",
|
| 136 |
"trufflehog"
|
| 137 |
],
|
| 138 |
+
"description": "TruffleHog v3 \u2014 high-signal secret scanning with 700+ detectors across git history"
|
| 139 |
},
|
| 140 |
"bandit-sast": {
|
| 141 |
"command": "uvx",
|
|
|
|
| 144 |
"--allow-commands",
|
| 145 |
"bandit"
|
| 146 |
],
|
| 147 |
+
"description": "Bandit Python SAST \u2014 AST-level detection of dangerous patterns, injection sinks, insecure APIs"
|
| 148 |
},
|
| 149 |
"pip-audit-sca": {
|
| 150 |
"command": "uvx",
|
|
|
|
| 153 |
"--allow-commands",
|
| 154 |
"pip-audit,pip"
|
| 155 |
],
|
| 156 |
+
"description": "pip-audit SCA \u2014 known vulnerabilities in Python dependencies via OSV and PyPI Advisory DB"
|
| 157 |
},
|
| 158 |
"osv-scanner": {
|
| 159 |
"command": "uvx",
|
|
|
|
| 162 |
"--allow-commands",
|
| 163 |
"osv-scanner"
|
| 164 |
],
|
| 165 |
+
"description": "OSV Scanner \u2014 multi-ecosystem SCA using the Open Source Vulnerability database (Google)"
|
| 166 |
},
|
| 167 |
"z3-formal-verifier": {
|
| 168 |
"command": "uvx",
|
|
|
|
| 171 |
"--allow-commands",
|
| 172 |
"python3"
|
| 173 |
],
|
| 174 |
+
"description": "Z3 SMT solver \u2014 formal verification of integer bounds, overflow invariants, protocol properties"
|
| 175 |
},
|
| 176 |
"hypothesis-fuzzer": {
|
| 177 |
"command": "uvx",
|
|
|
|
| 180 |
"--allow-commands",
|
| 181 |
"python3,pytest,hypothesis"
|
| 182 |
],
|
| 183 |
+
"description": "Hypothesis PBT fuzzer \u2014 property-based testing for arithmetic overflow, encoding, aliasing bugs"
|
| 184 |
},
|
| 185 |
"atheris-fuzzer": {
|
| 186 |
"command": "uvx",
|
|
|
|
| 189 |
"--allow-commands",
|
| 190 |
"python3,atheris"
|
| 191 |
],
|
| 192 |
+
"description": "Atheris coverage-guided fuzzer \u2014 libFuzzer-backed Python fuzzing for parser and protocol bugs"
|
| 193 |
},
|
| 194 |
"angr-symbolic": {
|
| 195 |
"command": "uvx",
|
|
|
|
| 198 |
"--allow-commands",
|
| 199 |
"python3"
|
| 200 |
],
|
| 201 |
+
"description": "angr symbolic execution \u2014 binary analysis, path exploration, constraint solving for native exploits"
|
| 202 |
},
|
| 203 |
"radon-complexity": {
|
| 204 |
"command": "uvx",
|
|
|
|
| 207 |
"--allow-commands",
|
| 208 |
"radon"
|
| 209 |
],
|
| 210 |
+
"description": "Radon AST complexity analysis \u2014 cyclomatic complexity, Halstead metrics, attack surface ranking"
|
| 211 |
},
|
| 212 |
"ruff-linter": {
|
| 213 |
"command": "uvx",
|
|
|
|
| 216 |
"--allow-commands",
|
| 217 |
"ruff"
|
| 218 |
],
|
| 219 |
+
"description": "Ruff ultra-fast Python linter \u2014 detects anti-patterns that correlate with security bugs"
|
| 220 |
},
|
| 221 |
"aider-patcher": {
|
| 222 |
"command": "uvx",
|
|
|
|
| 225 |
"--allow-commands",
|
| 226 |
"aider"
|
| 227 |
],
|
| 228 |
+
"description": "Aider AI code editor \u2014 applies LLM-generated patches with diff verification and test re-run",
|
| 229 |
"env": {
|
| 230 |
"OPENROUTER_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
| 231 |
}
|
|
|
|
| 235 |
"args": [
|
| 236 |
"mcp-server-fetch"
|
| 237 |
],
|
| 238 |
+
"description": "NVD/NIST CVE API \u2014 fetch full CVE details, CVSS vectors, CWE mappings, affected versions",
|
| 239 |
"env": {
|
| 240 |
"FETCH_ALLOWED_DOMAINS": "nvd.nist.gov,cve.org,cve.mitre.org,www.cvedetails.com,vulners.com,osv.dev,opencve.io",
|
| 241 |
"NVD_API_KEY": "__INJECTED_BY_APP_AT_RUNTIME__"
|
|
|
|
| 246 |
"args": [
|
| 247 |
"mcp-server-fetch"
|
| 248 |
],
|
| 249 |
+
"description": "Bug bounty platform APIs \u2014 HackerOne report submission, GitHub Security Advisories, Bugcrowd",
|
| 250 |
"env": {
|
| 251 |
"FETCH_ALLOWED_DOMAINS": "api.hackerone.com,api.bugcrowd.com,api.intigriti.com,api.yeswehack.com,api.github.com",
|
| 252 |
"HACKERONE_API_TOKEN": "__INJECTED_BY_APP_AT_RUNTIME__",
|
|
|
|
| 258 |
"args": [
|
| 259 |
"mcp-server-fetch"
|
| 260 |
],
|
| 261 |
+
"description": "Supply chain security \u2014 PyPI typosquatting, dependency confusion, malicious package detection",
|
| 262 |
"env": {
|
| 263 |
"FETCH_ALLOWED_DOMAINS": "pypi.org,api.pypi.org,registry.npmjs.org,crates.io,deps.dev,socket.dev,api.socket.dev"
|
| 264 |
}
|
|
|
|
| 277 |
"-m",
|
| 278 |
"mythos.mcp.static_analysis_mcp"
|
| 279 |
],
|
| 280 |
+
"description": "Mythos: Tree-sitter CPG, Joern, CodeQL, Semgrep \u2014 deep semantic static analysis"
|
| 281 |
},
|
| 282 |
"dynamic-analysis-mcp": {
|
| 283 |
"command": "python",
|
|
|
|
| 285 |
"-m",
|
| 286 |
"mythos.mcp.dynamic_analysis_mcp"
|
| 287 |
],
|
| 288 |
+
"description": "Mythos: AFL++, KLEE, QEMU, Frida, GDB \u2014 coverage-guided + symbolic + instrumented dynamic analysis"
|
| 289 |
},
|
| 290 |
"exploit-generation-mcp": {
|
| 291 |
"command": "python",
|
|
|
|
| 293 |
"-m",
|
| 294 |
"mythos.mcp.exploit_generation_mcp"
|
| 295 |
],
|
| 296 |
+
"description": "Mythos: Pwntools, ROPGadget, heap kit, privesc KB \u2014 autonomous PoC synthesis"
|
| 297 |
},
|
| 298 |
"vulnerability-database-mcp": {
|
| 299 |
"command": "python",
|
|
|
|
| 406 |
"-m",
|
| 407 |
"mythos.mcp.skill_selector_mcp"
|
| 408 |
],
|
| 409 |
+
"description": "Rhodawk: semantic skill selection (MiniLM) \u2014 brain extension on demand"
|
| 410 |
},
|
| 411 |
"trufflehog-deep-mcp": {
|
| 412 |
"command": "uvx",
|
|
|
|
| 424 |
"--allow-commands",
|
| 425 |
"gitleaks,git"
|
| 426 |
],
|
| 427 |
+
"description": "Gitleaks \u2014 complementary secret scanner with custom rules"
|
| 428 |
},
|
| 429 |
"semgrep-pro-patterns-mcp": {
|
| 430 |
"command": "uvx",
|
|
|
|
| 506 |
"description": "JS prototype-pollution sink scanner (AST-style grep)"
|
| 507 |
}
|
| 508 |
}
|
| 509 |
+
}
|
night_hunt_lock.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk AI — Night Hunt Mutual Exclusion Lock
|
| 3 |
+
==============================================
|
| 4 |
+
Resolves W-009 (MEDIUM): two entirely separate autonomous bug-bounty hunting
|
| 5 |
+
systems exist (`night_hunt_orchestrator.py` and `architect/nightmode.py`). Both
|
| 6 |
+
can be enabled simultaneously and both scan the same bounty platform scope
|
| 7 |
+
(HackerOne, Bugcrowd, Intigriti) with no deduplication or coordination.
|
| 8 |
+
|
| 9 |
+
This module exposes a single in-process re-entrant lock that BOTH orchestrators
|
| 10 |
+
must acquire before running a hunt cycle. Whichever loop wakes up first holds
|
| 11 |
+
the lock for the duration of its cycle; the other simply skips this round and
|
| 12 |
+
sleeps until its next scheduled wake.
|
| 13 |
+
|
| 14 |
+
Cross-process protection (multi-container deployments) should layer a
|
| 15 |
+
SQLite/Postgres advisory lock on top of this, but for the single-container HF
|
| 16 |
+
Spaces deployment the in-process lock is sufficient.
|
| 17 |
+
|
| 18 |
+
Usage:
|
| 19 |
+
|
| 20 |
+
from night_hunt_lock import try_acquire_night_hunt, release_night_hunt
|
| 21 |
+
|
| 22 |
+
if not try_acquire_night_hunt("architect-nightmode"):
|
| 23 |
+
LOG.info("another night-hunt loop is already running; skipping cycle")
|
| 24 |
+
return
|
| 25 |
+
try:
|
| 26 |
+
run_one_cycle()
|
| 27 |
+
finally:
|
| 28 |
+
release_night_hunt("architect-nightmode")
|
| 29 |
+
|
| 30 |
+
Or as a context manager:
|
| 31 |
+
|
| 32 |
+
from night_hunt_lock import night_hunt_guard
|
| 33 |
+
with night_hunt_guard("night-hunt-orchestrator") as acquired:
|
| 34 |
+
if not acquired:
|
| 35 |
+
return
|
| 36 |
+
run_night_cycle()
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
from __future__ import annotations
|
| 40 |
+
|
| 41 |
+
import os
|
| 42 |
+
import threading
|
| 43 |
+
import time
|
| 44 |
+
from contextlib import contextmanager
|
| 45 |
+
|
| 46 |
+
# Operators may opt out of the cross-loop guard if they truly want both
|
| 47 |
+
# loops to run independently (not recommended). Default: enabled.
|
| 48 |
+
_ENABLED = os.getenv("RHODAWK_NIGHT_HUNT_LOCK", "true").lower() == "true"
|
| 49 |
+
|
| 50 |
+
_LOCK = threading.Lock()
|
| 51 |
+
_HOLDER: str | None = None
|
| 52 |
+
_ACQUIRED_AT: float = 0.0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def is_locked() -> tuple[bool, str | None, float]:
|
| 56 |
+
"""Return (locked, holder_name, seconds_held)."""
|
| 57 |
+
with _LOCK:
|
| 58 |
+
if _HOLDER is None:
|
| 59 |
+
return (False, None, 0.0)
|
| 60 |
+
return (True, _HOLDER, time.time() - _ACQUIRED_AT)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def try_acquire_night_hunt(holder: str) -> bool:
|
| 64 |
+
"""Non-blocking acquire. Returns True if this caller now owns the lock."""
|
| 65 |
+
global _HOLDER, _ACQUIRED_AT
|
| 66 |
+
if not _ENABLED:
|
| 67 |
+
return True
|
| 68 |
+
with _LOCK:
|
| 69 |
+
if _HOLDER is not None:
|
| 70 |
+
return False
|
| 71 |
+
_HOLDER = holder
|
| 72 |
+
_ACQUIRED_AT = time.time()
|
| 73 |
+
return True
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def release_night_hunt(holder: str) -> None:
|
| 77 |
+
"""Release the lock. Only the current holder may release."""
|
| 78 |
+
global _HOLDER, _ACQUIRED_AT
|
| 79 |
+
if not _ENABLED:
|
| 80 |
+
return
|
| 81 |
+
with _LOCK:
|
| 82 |
+
if _HOLDER == holder:
|
| 83 |
+
_HOLDER = None
|
| 84 |
+
_ACQUIRED_AT = 0.0
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
@contextmanager
|
| 88 |
+
def night_hunt_guard(holder: str):
|
| 89 |
+
"""Context manager that yields True if the lock was acquired."""
|
| 90 |
+
acquired = try_acquire_night_hunt(holder)
|
| 91 |
+
try:
|
| 92 |
+
yield acquired
|
| 93 |
+
finally:
|
| 94 |
+
if acquired:
|
| 95 |
+
release_night_hunt(holder)
|
night_hunt_orchestrator.py
CHANGED
|
@@ -452,6 +452,28 @@ def run_night_cycle(
|
|
| 452 |
max_targets: int = MAX_TARGETS,
|
| 453 |
) -> NightCycleReport:
|
| 454 |
"""Execute one full hunting cycle and return the report."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
cycle_id = uuid.uuid4().hex[:12]
|
| 456 |
report = NightCycleReport(
|
| 457 |
cycle_id=cycle_id,
|
|
@@ -471,19 +493,23 @@ def run_night_cycle(
|
|
| 471 |
report.errors.append(f"scope:{exc}")
|
| 472 |
return _finalise(report)
|
| 473 |
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
f
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
|
|
|
| 485 |
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
|
| 489 |
def _finalise(report: NightCycleReport) -> NightCycleReport:
|
|
|
|
| 452 |
max_targets: int = MAX_TARGETS,
|
| 453 |
) -> NightCycleReport:
|
| 454 |
"""Execute one full hunting cycle and return the report."""
|
| 455 |
+
# W-009 FIX: serialize against architect/nightmode.py so the two
|
| 456 |
+
# autonomous bug-bounty loops never run overlapping cycles against the
|
| 457 |
+
# same bounty platform scope.
|
| 458 |
+
from night_hunt_lock import try_acquire_night_hunt, release_night_hunt, is_locked
|
| 459 |
+
|
| 460 |
+
holder = "night-hunt-orchestrator"
|
| 461 |
+
if not try_acquire_night_hunt(holder):
|
| 462 |
+
locked, other, held_for = is_locked()
|
| 463 |
+
LOG.warning(
|
| 464 |
+
"night cycle skipped — another hunt loop (%s) holds the lock "
|
| 465 |
+
"for %.0fs already. Set RHODAWK_NIGHT_HUNT_LOCK=false to disable "
|
| 466 |
+
"this guard.", other, held_for,
|
| 467 |
+
)
|
| 468 |
+
skip_report = NightCycleReport(
|
| 469 |
+
cycle_id=uuid.uuid4().hex[:12],
|
| 470 |
+
started_at=datetime.now(timezone.utc).isoformat(),
|
| 471 |
+
platforms=list(platforms or DEFAULT_PLATFORMS),
|
| 472 |
+
)
|
| 473 |
+
skip_report.notes.append(f"skipped: night-hunt lock held by {other}")
|
| 474 |
+
skip_report.finished_at = datetime.now(timezone.utc).isoformat()
|
| 475 |
+
return skip_report
|
| 476 |
+
|
| 477 |
cycle_id = uuid.uuid4().hex[:12]
|
| 478 |
report = NightCycleReport(
|
| 479 |
cycle_id=cycle_id,
|
|
|
|
| 493 |
report.errors.append(f"scope:{exc}")
|
| 494 |
return _finalise(report)
|
| 495 |
|
| 496 |
+
try:
|
| 497 |
+
for tgt in report.targets:
|
| 498 |
+
try:
|
| 499 |
+
recon = _recon(tgt)
|
| 500 |
+
findings = _hunt(tgt, recon)
|
| 501 |
+
findings = _validate(findings)
|
| 502 |
+
for f in findings:
|
| 503 |
+
f.draft_submission = _draft_submission(f)
|
| 504 |
+
report.findings.extend(findings)
|
| 505 |
+
except Exception as exc: # noqa: BLE001
|
| 506 |
+
LOG.exception("target %s crashed: %s", tgt.program, exc)
|
| 507 |
+
report.errors.append(f"target:{tgt.program}:{exc}")
|
| 508 |
|
| 509 |
+
return _finalise(report)
|
| 510 |
+
finally:
|
| 511 |
+
# W-009 FIX: always release the lock so subsequent cycles can run.
|
| 512 |
+
release_night_hunt(holder)
|
| 513 |
|
| 514 |
|
| 515 |
def _finalise(report: NightCycleReport) -> NightCycleReport:
|
repo_harvester.py
CHANGED
|
@@ -22,10 +22,14 @@ import os
|
|
| 22 |
import threading
|
| 23 |
import time
|
| 24 |
from dataclasses import dataclass, field, asdict
|
|
|
|
| 25 |
from typing import Optional
|
| 26 |
|
| 27 |
import requests
|
| 28 |
|
|
|
|
|
|
|
|
|
|
| 29 |
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "")
|
| 30 |
HARVESTER_ENABLED = os.getenv("RHODAWK_HARVESTER_ENABLED", "false").lower() == "true"
|
| 31 |
HARVESTER_POLL_S = int(os.getenv("RHODAWK_HARVESTER_POLL_SECONDS", "21600"))
|
|
@@ -67,10 +71,14 @@ def _gh_headers() -> dict:
|
|
| 67 |
|
| 68 |
def _search_repos_with_failing_ci(language: str, page: int = 1) -> list[dict]:
|
| 69 |
"""Search GitHub for repos in a given language with recent activity."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
q = (
|
| 71 |
f"language:{language} "
|
| 72 |
f"stars:>={HARVESTER_MIN_STARS} "
|
| 73 |
-
f"pushed:>
|
| 74 |
f"fork:false "
|
| 75 |
f"is:public"
|
| 76 |
)
|
|
|
|
| 22 |
import threading
|
| 23 |
import time
|
| 24 |
from dataclasses import dataclass, field, asdict
|
| 25 |
+
from datetime import datetime, timedelta, timezone
|
| 26 |
from typing import Optional
|
| 27 |
|
| 28 |
import requests
|
| 29 |
|
| 30 |
+
# W-010 FIX: rolling 30-day window instead of a hardcoded 2025-01-01 floor.
|
| 31 |
+
HARVESTER_PUSHED_WINDOW_DAYS = int(os.getenv("RHODAWK_HARVESTER_PUSHED_WINDOW_DAYS", "30"))
|
| 32 |
+
|
| 33 |
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "")
|
| 34 |
HARVESTER_ENABLED = os.getenv("RHODAWK_HARVESTER_ENABLED", "false").lower() == "true"
|
| 35 |
HARVESTER_POLL_S = int(os.getenv("RHODAWK_HARVESTER_POLL_SECONDS", "21600"))
|
|
|
|
| 71 |
|
| 72 |
def _search_repos_with_failing_ci(language: str, page: int = 1) -> list[dict]:
|
| 73 |
"""Search GitHub for repos in a given language with recent activity."""
|
| 74 |
+
# W-010 FIX: dynamic rolling window — was hardcoded "pushed:>2025-01-01".
|
| 75 |
+
pushed_floor = (
|
| 76 |
+
datetime.now(timezone.utc) - timedelta(days=HARVESTER_PUSHED_WINDOW_DAYS)
|
| 77 |
+
).strftime("%Y-%m-%d")
|
| 78 |
q = (
|
| 79 |
f"language:{language} "
|
| 80 |
f"stars:>={HARVESTER_MIN_STARS} "
|
| 81 |
+
f"pushed:>{pushed_floor} "
|
| 82 |
f"fork:false "
|
| 83 |
f"is:public"
|
| 84 |
)
|
scripts/generate_stubs.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# ============================================================================
|
| 3 |
+
# Rhodawk AI — Local gRPC Stub Generator
|
| 4 |
+
# ----------------------------------------------------------------------------
|
| 5 |
+
# Resolves W-001 (CRITICAL): the openclaude_grpc/openclaude_pb2.py and
|
| 6 |
+
# openclaude_grpc/openclaude_pb2_grpc.py files are NOT committed to source
|
| 7 |
+
# (they are generated at Docker build time). This script generates them
|
| 8 |
+
# locally so app.py can be imported and run without a full Docker build.
|
| 9 |
+
#
|
| 10 |
+
# Usage:
|
| 11 |
+
# bash scripts/generate_stubs.sh
|
| 12 |
+
#
|
| 13 |
+
# Requirements:
|
| 14 |
+
# pip install grpcio-tools
|
| 15 |
+
# ============================================================================
|
| 16 |
+
set -euo pipefail
|
| 17 |
+
|
| 18 |
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 19 |
+
PROTO_DIR="${REPO_ROOT}/vendor/openclaude/src/proto"
|
| 20 |
+
OUT_DIR="${REPO_ROOT}/openclaude_grpc"
|
| 21 |
+
|
| 22 |
+
if [ ! -f "${PROTO_DIR}/openclaude.proto" ]; then
|
| 23 |
+
echo "[generate_stubs] ERROR: ${PROTO_DIR}/openclaude.proto not found." >&2
|
| 24 |
+
echo "[generate_stubs] The vendor/openclaude submodule may be missing." >&2
|
| 25 |
+
exit 1
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
if ! python -c "import grpc_tools" 2>/dev/null; then
|
| 29 |
+
echo "[generate_stubs] ERROR: grpcio-tools not installed." >&2
|
| 30 |
+
echo "[generate_stubs] Run: pip install grpcio-tools" >&2
|
| 31 |
+
exit 1
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
mkdir -p "${OUT_DIR}"
|
| 35 |
+
|
| 36 |
+
echo "[generate_stubs] Generating Python gRPC stubs..."
|
| 37 |
+
python -m grpc_tools.protoc \
|
| 38 |
+
-I "${PROTO_DIR}" \
|
| 39 |
+
--python_out="${OUT_DIR}" \
|
| 40 |
+
--grpc_python_out="${OUT_DIR}" \
|
| 41 |
+
"${PROTO_DIR}/openclaude.proto"
|
| 42 |
+
|
| 43 |
+
# Patch the generated _grpc.py to use a relative import so it works as a
|
| 44 |
+
# package module (the protoc default emits an absolute import).
|
| 45 |
+
GRPC_FILE="${OUT_DIR}/openclaude_pb2_grpc.py"
|
| 46 |
+
if [ -f "${GRPC_FILE}" ]; then
|
| 47 |
+
sed -i.bak 's/^import openclaude_pb2 as openclaude__pb2$/from . import openclaude_pb2 as openclaude__pb2/' "${GRPC_FILE}"
|
| 48 |
+
rm -f "${GRPC_FILE}.bak"
|
| 49 |
+
fi
|
| 50 |
+
|
| 51 |
+
echo "[generate_stubs] Done. Generated:"
|
| 52 |
+
ls -la "${OUT_DIR}"/openclaude_pb2*.py
|
training_store.py
CHANGED
|
@@ -44,8 +44,35 @@ def _get_conn():
|
|
| 44 |
return cur
|
| 45 |
|
| 46 |
def executescript(self, script: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
cur = self.conn.cursor()
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return cur
|
| 50 |
|
| 51 |
def commit(self):
|
|
|
|
| 44 |
return cur
|
| 45 |
|
| 46 |
def executescript(self, script: str):
|
| 47 |
+
# W-006 FIX: psycopg2's cursor.execute() only processes the
|
| 48 |
+
# FIRST statement of a multi-statement string. SQLite's
|
| 49 |
+
# connection.executescript() processes all of them. To match
|
| 50 |
+
# SQLite semantics, split on `;` and execute statements one at
|
| 51 |
+
# a time. We respect single/double-quoted string literals so
|
| 52 |
+
# semicolons inside SQL strings (e.g. defaults) don't break
|
| 53 |
+
# the split.
|
| 54 |
cur = self.conn.cursor()
|
| 55 |
+
statements: list[str] = []
|
| 56 |
+
buf: list[str] = []
|
| 57 |
+
in_squote = False
|
| 58 |
+
in_dquote = False
|
| 59 |
+
for ch in script:
|
| 60 |
+
if ch == "'" and not in_dquote:
|
| 61 |
+
in_squote = not in_squote
|
| 62 |
+
elif ch == '"' and not in_squote:
|
| 63 |
+
in_dquote = not in_dquote
|
| 64 |
+
if ch == ";" and not in_squote and not in_dquote:
|
| 65 |
+
stmt = "".join(buf).strip()
|
| 66 |
+
if stmt:
|
| 67 |
+
statements.append(stmt)
|
| 68 |
+
buf = []
|
| 69 |
+
else:
|
| 70 |
+
buf.append(ch)
|
| 71 |
+
tail = "".join(buf).strip()
|
| 72 |
+
if tail:
|
| 73 |
+
statements.append(tail)
|
| 74 |
+
for stmt in statements:
|
| 75 |
+
cur.execute(stmt)
|
| 76 |
return cur
|
| 77 |
|
| 78 |
def commit(self):
|
webhook_server.py
CHANGED
|
@@ -187,6 +187,20 @@ class WebhookHandler(BaseHTTPRequestHandler):
|
|
| 187 |
|
| 188 |
if path == "/webhook/github":
|
| 189 |
sig = self.headers.get("X-Hub-Signature-256", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
if not _verify_github_signature(body, sig):
|
| 191 |
_log_webhook("github", payload, "REJECTED", "Invalid HMAC signature")
|
| 192 |
self._send_json(401, {"error": "Invalid signature"})
|
|
|
|
| 187 |
|
| 188 |
if path == "/webhook/github":
|
| 189 |
sig = self.headers.get("X-Hub-Signature-256", "")
|
| 190 |
+
# W-007 FIX: distinguish missing-secret config from a bad signature.
|
| 191 |
+
# When RHODAWK_WEBHOOK_SECRET is unset we now return 403 with an
|
| 192 |
+
# explicit JSON error body so the operator gets an actionable
|
| 193 |
+
# response instead of a silent rejection.
|
| 194 |
+
if not WEBHOOK_SECRET:
|
| 195 |
+
_log_webhook("github", payload, "REJECTED",
|
| 196 |
+
"RHODAWK_WEBHOOK_SECRET not configured on server")
|
| 197 |
+
self._send_json(403, {
|
| 198 |
+
"error": "RHODAWK_WEBHOOK_SECRET not configured on server",
|
| 199 |
+
"remediation": "Set the RHODAWK_WEBHOOK_SECRET environment variable "
|
| 200 |
+
"to the same shared secret configured on your GitHub "
|
| 201 |
+
"webhook before sending events.",
|
| 202 |
+
})
|
| 203 |
+
return
|
| 204 |
if not _verify_github_signature(body, sig):
|
| 205 |
_log_webhook("github", payload, "REJECTED", "Invalid HMAC signature")
|
| 206 |
self._send_json(401, {"error": "Invalid signature"})
|