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 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
- Z3_ENABLED = os.getenv("RHODAWK_Z3_ENABLED", "false").lower() == "true"
 
 
 
 
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
- if DO_INFERENCE_API_KEY:
 
 
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 contains NO secrets.",
4
  "Actual runtime config is written to /tmp/mcp_runtime.json at startup.",
5
- "Secrets injected from HF Space env vars never committed.",
6
  "FETCH_ALLOWED_DOMAINS prevents SSRF against internal services.",
7
- "Rhodawk AI v6.0 37 cybersecurity MCP server suite (12 new in v6 per Masterplan §8)."
8
  ],
9
  "mcpServers": {
10
  "fetch-docs": {
@@ -45,7 +55,7 @@
45
  "-y",
46
  "@modelcontextprotocol/server-memory"
47
  ],
48
- "description": "Persistent knowledge graph stores exploit chains, CWE patterns, and cross-session vulnerability memory"
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 fast queries on vulnerability metadata, CVSS scores, and bounty estimates"
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 DAST, CVE detection, misconfig scanning",
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 taint analysis, CWE pattern matching, secrets detection across 30+ languages",
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 high-signal secret scanning with 700+ detectors across git history"
129
  },
130
  "bandit-sast": {
131
  "command": "uvx",
@@ -134,7 +144,7 @@
134
  "--allow-commands",
135
  "bandit"
136
  ],
137
- "description": "Bandit Python SAST AST-level detection of dangerous patterns, injection sinks, insecure APIs"
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 known vulnerabilities in Python dependencies via OSV and PyPI Advisory DB"
147
  },
148
  "osv-scanner": {
149
  "command": "uvx",
@@ -152,7 +162,7 @@
152
  "--allow-commands",
153
  "osv-scanner"
154
  ],
155
- "description": "OSV Scanner multi-ecosystem SCA using the Open Source Vulnerability database (Google)"
156
  },
157
  "z3-formal-verifier": {
158
  "command": "uvx",
@@ -161,7 +171,7 @@
161
  "--allow-commands",
162
  "python3"
163
  ],
164
- "description": "Z3 SMT solver formal verification of integer bounds, overflow invariants, protocol properties"
165
  },
166
  "hypothesis-fuzzer": {
167
  "command": "uvx",
@@ -170,7 +180,7 @@
170
  "--allow-commands",
171
  "python3,pytest,hypothesis"
172
  ],
173
- "description": "Hypothesis PBT fuzzer property-based testing for arithmetic overflow, encoding, aliasing bugs"
174
  },
175
  "atheris-fuzzer": {
176
  "command": "uvx",
@@ -179,7 +189,7 @@
179
  "--allow-commands",
180
  "python3,atheris"
181
  ],
182
- "description": "Atheris coverage-guided fuzzer libFuzzer-backed Python fuzzing for parser and protocol bugs"
183
  },
184
  "angr-symbolic": {
185
  "command": "uvx",
@@ -188,7 +198,7 @@
188
  "--allow-commands",
189
  "python3"
190
  ],
191
- "description": "angr symbolic execution binary analysis, path exploration, constraint solving for native exploits"
192
  },
193
  "radon-complexity": {
194
  "command": "uvx",
@@ -197,7 +207,7 @@
197
  "--allow-commands",
198
  "radon"
199
  ],
200
- "description": "Radon AST complexity analysis cyclomatic complexity, Halstead metrics, attack surface ranking"
201
  },
202
  "ruff-linter": {
203
  "command": "uvx",
@@ -206,7 +216,7 @@
206
  "--allow-commands",
207
  "ruff"
208
  ],
209
- "description": "Ruff ultra-fast Python linter detects anti-patterns that correlate with security bugs"
210
  },
211
  "aider-patcher": {
212
  "command": "uvx",
@@ -215,7 +225,7 @@
215
  "--allow-commands",
216
  "aider"
217
  ],
218
- "description": "Aider AI code editor applies LLM-generated patches with diff verification and test re-run",
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 fetch full CVE details, CVSS vectors, CWE mappings, affected versions",
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 HackerOne report submission, GitHub Security Advisories, Bugcrowd",
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 PyPI typosquatting, dependency confusion, malicious package detection",
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 deep semantic static analysis"
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 coverage-guided + symbolic + instrumented dynamic analysis"
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 autonomous PoC synthesis"
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) brain extension on demand"
400
  },
401
  "trufflehog-deep-mcp": {
402
  "command": "uvx",
@@ -414,7 +424,7 @@
414
  "--allow-commands",
415
  "gitleaks,git"
416
  ],
417
- "description": "Gitleaks complementary secret scanner with custom rules"
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
- for tgt in report.targets:
475
- try:
476
- recon = _recon(tgt)
477
- findings = _hunt(tgt, recon)
478
- findings = _validate(findings)
479
- for f in findings:
480
- f.draft_submission = _draft_submission(f)
481
- report.findings.extend(findings)
482
- except Exception as exc: # noqa: BLE001
483
- LOG.exception("target %s crashed: %s", tgt.program, exc)
484
- report.errors.append(f"target:{tgt.program}:{exc}")
 
485
 
486
- return _finalise(report)
 
 
 
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:>2025-01-01 "
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
- cur.execute(script)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"})