Rhodawk Agent commited on
Commit Β·
59dc700
1
Parent(s): c664730
Fix: build error + ACTS consensus + ChainAnalyzer + rate limit + timer + venv path\n\n Build error fix:\n - requirements.txt: Remove atheris (requires Clang+libFuzzer, unavailable in HF Spaces)\n - fuzzing_engine.py: Graceful atheris fallback, detects availability at runtime,\n falls back to Hypothesis automatically\n - Dockerfile: Renamed builder stage to base, runtime inherits directly, eliminates\n broken reference to nonexistent 'builder' stage name in COPY --from\n\n ACTS Consensus bug (hermes_orchestrator.py):\n - _run_acts_consensus now calls all 3 models individually and concurrently,\n passing all 3 raw verdicts to compute_acts() so disagreement penalty works\n - Previously merged result collapsed to 1-item list (agreement_factor always 1.0)\n\n ChainAnalyzerTool in Hermes registry (hermes_orchestrator.py):\n - Added ChainAnalyzerTool with name=chain_analysis registered in _TOOL_REGISTRY\n - Hermes can now autonomously synthesize exploit chains without human UI click\n - _HERMES_SYSTEM prompt updated to document the new tool\n\n LLM rate limit backoff (hermes_orchestrator.py):\n - _hermes_llm_call retries on 429 with delays 15s/30s/60s\n - Single 429 no longer aborts the entire research session\n\n Timer consolidation (app.py):\n - get_combined_refresh() returns all 8 live-update outputs at once\n - 3 concurrent gr.Timer.tick() SSE streams collapsed to single tick\n - Reduces HF Space connection pressure under multiple simultaneous users\n\n Venv path fix (app.py):\n - execute_approved_harness uses VENV_DIR constant, not hardcoded string\n - Creates venv on-demand if missing so standalone Security Research tab works
Browse files- Dockerfile +10 -23
- app.py +0 -0
- fuzzing_engine.py +51 -12
- hermes_orchestrator.py +98 -16
- requirements.txt +2 -1
Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Stage 1: Builder
|
| 2 |
-
FROM python:3.12-slim AS
|
| 3 |
|
| 4 |
ENV DEBIAN_FRONTEND=noninteractive \
|
| 5 |
PYTHONUNBUFFERED=1 \
|
|
@@ -16,33 +16,26 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
| 16 |
WORKDIR /build
|
| 17 |
COPY requirements.txt .
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
#
|
| 21 |
-
#
|
| 22 |
-
RUN pip
|
| 23 |
|
| 24 |
|
| 25 |
-
# Stage 2: Runtime
|
| 26 |
-
FROM
|
| 27 |
|
| 28 |
LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine"
|
| 29 |
|
| 30 |
-
|
| 31 |
-
# rather than /usr/local/bin/python which may lack the executable in some
|
| 32 |
-
# HuggingFace Space runtime snapshots. UV_PYTHON_PREFERENCE=system tells uv
|
| 33 |
-
# to skip its managed-toolchain download and use the container Python directly.
|
| 34 |
-
ENV DEBIAN_FRONTEND=noninteractive \
|
| 35 |
-
PYTHONUNBUFFERED=1 \
|
| 36 |
-
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 37 |
GRADIO_SERVER_PORT=7860 \
|
| 38 |
HOME=/home/rhodawk \
|
| 39 |
PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
|
| 40 |
-
UV_LINK_MODE=copy \
|
| 41 |
UV_PYTHON_PREFERENCE=system \
|
| 42 |
UV_PYTHON=/usr/local/bin/python3
|
| 43 |
|
| 44 |
RUN apt-get update && \
|
| 45 |
-
apt-get install -y --no-install-recommends
|
| 46 |
rm -rf /var/lib/apt/lists/*
|
| 47 |
|
| 48 |
RUN npm install -g --quiet @modelcontextprotocol/server-github
|
|
@@ -55,16 +48,10 @@ RUN id -u 1000 >/dev/null 2>&1 && (userdel -r $(id -un 1000) || true) || true &&
|
|
| 55 |
# even before the application calls os.makedirs() at runtime.
|
| 56 |
RUN mkdir -p /data /app && chmod 777 /data && chown -R rhodawk:rhodawk /app
|
| 57 |
|
| 58 |
-
WORKDIR /app
|
| 59 |
-
|
| 60 |
-
# Copy pre-built wheels from builder
|
| 61 |
-
COPY --from=builder /wheels /wheels
|
| 62 |
-
|
| 63 |
# Copy the uv executable from the official image
|
| 64 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
USER rhodawk
|
| 69 |
|
| 70 |
# Copy the source code
|
|
|
|
| 1 |
# Stage 1: Builder
|
| 2 |
+
FROM python:3.12-slim AS base
|
| 3 |
|
| 4 |
ENV DEBIAN_FRONTEND=noninteractive \
|
| 5 |
PYTHONUNBUFFERED=1 \
|
|
|
|
| 16 |
WORKDIR /build
|
| 17 |
COPY requirements.txt .
|
| 18 |
|
| 19 |
+
# Install dependencies directly β avoids wheel-build failures for packages
|
| 20 |
+
# that require special compile-time tooling (e.g. atheris/libFuzzer).
|
| 21 |
+
# atheris has been removed from requirements.txt; Hypothesis is the fallback.
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt mcp-server-fetch
|
| 23 |
|
| 24 |
|
| 25 |
+
# Stage 2: Runtime β inherits installed packages from base
|
| 26 |
+
FROM base AS runtime
|
| 27 |
|
| 28 |
LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine"
|
| 29 |
|
| 30 |
+
ENV GRADIO_SERVER_NAME=0.0.0.0 \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
GRADIO_SERVER_PORT=7860 \
|
| 32 |
HOME=/home/rhodawk \
|
| 33 |
PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
|
|
|
|
| 34 |
UV_PYTHON_PREFERENCE=system \
|
| 35 |
UV_PYTHON=/usr/local/bin/python3
|
| 36 |
|
| 37 |
RUN apt-get update && \
|
| 38 |
+
apt-get install -y --no-install-recommends nodejs npm && \
|
| 39 |
rm -rf /var/lib/apt/lists/*
|
| 40 |
|
| 41 |
RUN npm install -g --quiet @modelcontextprotocol/server-github
|
|
|
|
| 48 |
# even before the application calls os.makedirs() at runtime.
|
| 49 |
RUN mkdir -p /data /app && chmod 777 /data && chown -R rhodawk:rhodawk /app
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
# Copy the uv executable from the official image
|
| 52 |
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
| 53 |
|
| 54 |
+
WORKDIR /app
|
|
|
|
| 55 |
USER rhodawk
|
| 56 |
|
| 57 |
# Copy the source code
|
app.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
fuzzing_engine.py
CHANGED
|
@@ -152,21 +152,47 @@ def _extract_code_block(text: str) -> str:
|
|
| 152 |
def _fallback_harness(target: str, language: str) -> str:
|
| 153 |
"""Generic fallback harness when LLM is unavailable."""
|
| 154 |
if language == "python":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
return f"""
|
| 156 |
-
import atheris
|
| 157 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
except Exception:
|
| 166 |
-
pass
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
"""
|
| 171 |
return f"# Fallback harness for {target} ({language})\n# Manual harness required\n"
|
| 172 |
|
|
@@ -331,7 +357,20 @@ def run_fuzzing_campaign(
|
|
| 331 |
|
| 332 |
start = time.time()
|
| 333 |
if language == "python" and "atheris" in harness_code:
|
| 334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
else:
|
| 336 |
crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
|
| 337 |
|
|
|
|
| 152 |
def _fallback_harness(target: str, language: str) -> str:
|
| 153 |
"""Generic fallback harness when LLM is unavailable."""
|
| 154 |
if language == "python":
|
| 155 |
+
# FIX (Build Error): atheris requires Clang + libFuzzer at compile time and
|
| 156 |
+
# fails to build on HuggingFace Spaces. The fallback harness now uses
|
| 157 |
+
# hypothesis which is available everywhere, matching the Hypothesis
|
| 158 |
+
# fallback already in use when atheris is unavailable at runtime.
|
| 159 |
return f"""
|
|
|
|
| 160 |
import sys
|
| 161 |
+
try:
|
| 162 |
+
import atheris
|
| 163 |
+
_ATHERIS_AVAILABLE = True
|
| 164 |
+
except ImportError:
|
| 165 |
+
_ATHERIS_AVAILABLE = False
|
| 166 |
+
|
| 167 |
+
if _ATHERIS_AVAILABLE:
|
| 168 |
+
import sys
|
| 169 |
+
|
| 170 |
+
@atheris.instrument_func
|
| 171 |
+
def fuzz_target(data):
|
| 172 |
+
fdp = atheris.FuzzedDataProvider(data)
|
| 173 |
+
try:
|
| 174 |
+
val = fdp.ConsumeUnicodeNoSurrogates(128)
|
| 175 |
+
# TODO: call {target}(val)
|
| 176 |
+
except Exception:
|
| 177 |
+
pass
|
| 178 |
|
| 179 |
+
atheris.Setup(sys.argv, fuzz_target)
|
| 180 |
+
atheris.Fuzz()
|
| 181 |
+
else:
|
| 182 |
+
# Hypothesis-based fallback when atheris/libFuzzer is unavailable
|
| 183 |
+
from hypothesis import given, settings, HealthCheck
|
| 184 |
+
from hypothesis import strategies as st
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
@given(st.text(max_size=128))
|
| 187 |
+
@settings(max_examples=500, suppress_health_check=list(HealthCheck))
|
| 188 |
+
def fuzz_target(val):
|
| 189 |
+
try:
|
| 190 |
+
# TODO: call {target}(val)
|
| 191 |
+
pass
|
| 192 |
+
except Exception:
|
| 193 |
+
pass
|
| 194 |
+
|
| 195 |
+
fuzz_target()
|
| 196 |
"""
|
| 197 |
return f"# Fallback harness for {target} ({language})\n# Manual harness required\n"
|
| 198 |
|
|
|
|
| 357 |
|
| 358 |
start = time.time()
|
| 359 |
if language == "python" and "atheris" in harness_code:
|
| 360 |
+
# FIX (Build Error): atheris may be unavailable; fall back to Hypothesis.
|
| 361 |
+
try:
|
| 362 |
+
import importlib.util
|
| 363 |
+
_atheris_available = importlib.util.find_spec("atheris") is not None
|
| 364 |
+
except Exception:
|
| 365 |
+
_atheris_available = False
|
| 366 |
+
if _atheris_available:
|
| 367 |
+
crashes = _run_python_atheris(harness_code, duration_s)
|
| 368 |
+
else:
|
| 369 |
+
# Rewrite the harness to use Hypothesis if atheris is not installed
|
| 370 |
+
harness_code = harness_code.replace(
|
| 371 |
+
"import atheris", "# atheris unavailable β using Hypothesis fallback"
|
| 372 |
+
)
|
| 373 |
+
crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
|
| 374 |
else:
|
| 375 |
crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
|
| 376 |
|
hermes_orchestrator.py
CHANGED
|
@@ -210,10 +210,30 @@ class SSECTool(HermesTool):
|
|
| 210 |
return run_ssec_scan(repo_dir, focus_files=focus_files)
|
| 211 |
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
_TOOL_REGISTRY: dict[str, HermesTool] = {
|
| 214 |
t.name: t() for t in [
|
| 215 |
ReconTool, TaintTool, SymbolicTool, FuzzTool,
|
| 216 |
ExploitTool, CVETool, CommitWatchTool, SSECTool,
|
|
|
|
| 217 |
]
|
| 218 |
}
|
| 219 |
|
|
@@ -326,6 +346,7 @@ You have access to these tools:
|
|
| 326 |
- cve_intel: Query historical CVEs for similar patterns
|
| 327 |
- commit_watch: Find silent security patches in commit history
|
| 328 |
- ssec_scan: Semantic similarity to known exploit patterns
|
|
|
|
| 329 |
|
| 330 |
For each target, produce a research plan and execute it step by step.
|
| 331 |
When you find something, rate its severity honestly. Never hallucinate findings.
|
|
@@ -352,7 +373,14 @@ Or to signal completion:
|
|
| 352 |
"""
|
| 353 |
|
| 354 |
|
|
|
|
|
|
|
|
|
|
| 355 |
def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
if not OPENROUTER_API_KEY:
|
| 357 |
return {"done": True, "summary": "OPENROUTER_API_KEY not set"}
|
| 358 |
|
|
@@ -370,17 +398,34 @@ def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120
|
|
| 370 |
"max_tokens": 2048,
|
| 371 |
"response_format": {"type": "json_object"},
|
| 372 |
}
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
|
| 386 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -528,8 +573,17 @@ def run_hermes_research(
|
|
| 528 |
|
| 529 |
|
| 530 |
def _run_acts_consensus(session: HermesSession):
|
| 531 |
-
"""
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
CONSENSUS_MODELS = [
|
| 535 |
"deepseek/deepseek-r1:free",
|
|
@@ -551,12 +605,40 @@ def _run_acts_consensus(session: HermesSession):
|
|
| 551 |
"Is this a real, exploitable vulnerability? Respond as a hostile security reviewer."
|
| 552 |
)
|
| 553 |
try:
|
| 554 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
verdicts = [
|
| 556 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
]
|
| 558 |
finding.acts_score = compute_acts(verdicts)
|
| 559 |
-
hermes_log(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
except Exception as e:
|
| 561 |
hermes_log(f"ACTS consensus failed for {finding.finding_id}: {e}", "WARN")
|
| 562 |
finding.acts_score = finding.confidence
|
|
|
|
| 210 |
return run_ssec_scan(repo_dir, focus_files=focus_files)
|
| 211 |
|
| 212 |
|
| 213 |
+
class ChainAnalyzerTool(HermesTool):
|
| 214 |
+
name = "chain_analysis"
|
| 215 |
+
description = (
|
| 216 |
+
"Synthesize stored primitive findings into higher-severity exploit chains. "
|
| 217 |
+
"Call after at least 2 primitives are recorded. Returns THEORETICAL proposals "
|
| 218 |
+
"tagged PENDING_HUMAN_REVIEW β no chain is executed automatically."
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
def run(self, repo_dir: str, repo: str = "", **kwargs) -> dict:
|
| 222 |
+
hermes_log(f"Chain analysis β {repo or repo_dir}", "EXPLOIT")
|
| 223 |
+
from chain_analyzer import analyze_chains, get_all_primitives
|
| 224 |
+
target = repo or repo_dir
|
| 225 |
+
primitives = get_all_primitives(repo=target)
|
| 226 |
+
if len(primitives) < 2:
|
| 227 |
+
return {"chains": [], "note": f"Only {len(primitives)} primitive(s) stored β need β₯2 to chain"}
|
| 228 |
+
chains = analyze_chains(repo=target)
|
| 229 |
+
return {"chains": chains, "primitive_count": len(primitives)}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
_TOOL_REGISTRY: dict[str, HermesTool] = {
|
| 233 |
t.name: t() for t in [
|
| 234 |
ReconTool, TaintTool, SymbolicTool, FuzzTool,
|
| 235 |
ExploitTool, CVETool, CommitWatchTool, SSECTool,
|
| 236 |
+
ChainAnalyzerTool,
|
| 237 |
]
|
| 238 |
}
|
| 239 |
|
|
|
|
| 346 |
- cve_intel: Query historical CVEs for similar patterns
|
| 347 |
- commit_watch: Find silent security patches in commit history
|
| 348 |
- ssec_scan: Semantic similarity to known exploit patterns
|
| 349 |
+
- chain_analysis: Synthesize stored primitive findings into exploit chains (call after β₯2 findings)
|
| 350 |
|
| 351 |
For each target, produce a research plan and execute it step by step.
|
| 352 |
When you find something, rate its severity honestly. Never hallucinate findings.
|
|
|
|
| 373 |
"""
|
| 374 |
|
| 375 |
|
| 376 |
+
_RATE_LIMIT_BACKOFF_DELAYS = [15, 30, 60] # seconds β exponential backoff for 429s
|
| 377 |
+
|
| 378 |
+
|
| 379 |
def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120) -> dict:
|
| 380 |
+
"""
|
| 381 |
+
Call the Hermes LLM with exponential backoff on rate-limit (429) responses.
|
| 382 |
+
Three retries before giving up β prevents a single 429 from aborting a session.
|
| 383 |
+
"""
|
| 384 |
if not OPENROUTER_API_KEY:
|
| 385 |
return {"done": True, "summary": "OPENROUTER_API_KEY not set"}
|
| 386 |
|
|
|
|
| 398 |
"max_tokens": 2048,
|
| 399 |
"response_format": {"type": "json_object"},
|
| 400 |
}
|
| 401 |
+
|
| 402 |
+
last_error: Exception | None = None
|
| 403 |
+
for attempt, backoff in enumerate([0] + _RATE_LIMIT_BACKOFF_DELAYS):
|
| 404 |
+
if backoff:
|
| 405 |
+
hermes_log(f"Rate limit hit β waiting {backoff}s before retry {attempt}/{len(_RATE_LIMIT_BACKOFF_DELAYS)}", "WARN")
|
| 406 |
+
time.sleep(backoff)
|
| 407 |
+
try:
|
| 408 |
+
resp = requests.post(
|
| 409 |
+
f"{OPENROUTER_BASE}/chat/completions",
|
| 410 |
+
headers=headers, json=payload, timeout=timeout,
|
| 411 |
+
)
|
| 412 |
+
if resp.status_code == 429:
|
| 413 |
+
last_error = Exception(f"HTTP 429 rate limit (attempt {attempt + 1})")
|
| 414 |
+
hermes_log(str(last_error), "WARN")
|
| 415 |
+
continue
|
| 416 |
+
resp.raise_for_status()
|
| 417 |
+
content = resp.json()["choices"][0]["message"]["content"]
|
| 418 |
+
return json.loads(content)
|
| 419 |
+
except Exception as e:
|
| 420 |
+
if "429" in str(e):
|
| 421 |
+
last_error = e
|
| 422 |
+
hermes_log(f"Rate limit exception: {e}", "WARN")
|
| 423 |
+
continue
|
| 424 |
+
hermes_log(f"LLM call failed: {e}", "WARN")
|
| 425 |
+
return {"done": True, "summary": f"LLM error: {e}"}
|
| 426 |
+
|
| 427 |
+
hermes_log(f"LLM call exhausted all retries. Last error: {last_error}", "WARN")
|
| 428 |
+
return {"done": True, "summary": f"LLM rate limit β all retries exhausted: {last_error}"}
|
| 429 |
|
| 430 |
|
| 431 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 573 |
|
| 574 |
|
| 575 |
def _run_acts_consensus(session: HermesSession):
|
| 576 |
+
"""
|
| 577 |
+
Run multi-model adversarial consensus on each finding to compute ACTS score.
|
| 578 |
+
|
| 579 |
+
FIX (ACTS Bug): Previously _call_concurrent_consensus returned a single merged
|
| 580 |
+
result, so compute_acts() always received a 1-item list with agreement_factor=1.0
|
| 581 |
+
β completely bypassing the disagreement penalty. Now we call each consensus model
|
| 582 |
+
individually so all 3 raw verdicts are passed to compute_acts(), enabling the full
|
| 583 |
+
Bayesian disagreement weighting to work as designed.
|
| 584 |
+
"""
|
| 585 |
+
import concurrent.futures
|
| 586 |
+
from adversarial_reviewer import _call_single_model, ADVERSARY_SYSTEM_PROMPT
|
| 587 |
|
| 588 |
CONSENSUS_MODELS = [
|
| 589 |
"deepseek/deepseek-r1:free",
|
|
|
|
| 605 |
"Is this a real, exploitable vulnerability? Respond as a hostile security reviewer."
|
| 606 |
)
|
| 607 |
try:
|
| 608 |
+
# Call all 3 models concurrently and collect individual raw verdicts.
|
| 609 |
+
# This is required so compute_acts() receives the full disagreement signal
|
| 610 |
+
# rather than a pre-merged single verdict (which collapses agreement_factor to 1.0).
|
| 611 |
+
individual_results: list[dict] = []
|
| 612 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(CONSENSUS_MODELS)) as ex:
|
| 613 |
+
futures = {
|
| 614 |
+
ex.submit(_call_single_model, model, prompt): model
|
| 615 |
+
for model in CONSENSUS_MODELS
|
| 616 |
+
}
|
| 617 |
+
for future in concurrent.futures.as_completed(futures, timeout=90):
|
| 618 |
+
try:
|
| 619 |
+
result_dict, _ = future.result()
|
| 620 |
+
if result_dict is not None:
|
| 621 |
+
individual_results.append(result_dict)
|
| 622 |
+
except Exception:
|
| 623 |
+
pass
|
| 624 |
+
|
| 625 |
+
if not individual_results:
|
| 626 |
+
raise RuntimeError("All ACTS consensus models failed")
|
| 627 |
+
|
| 628 |
+
# Build one verdict entry per model response so compute_acts sees N items.
|
| 629 |
verdicts = [
|
| 630 |
+
{
|
| 631 |
+
"verdict": r.get("verdict", "UNCERTAIN"),
|
| 632 |
+
"confidence": float(r.get("confidence", 0.5)),
|
| 633 |
+
}
|
| 634 |
+
for r in individual_results
|
| 635 |
]
|
| 636 |
finding.acts_score = compute_acts(verdicts)
|
| 637 |
+
hermes_log(
|
| 638 |
+
f"ACTS score for {finding.finding_id}: {finding.acts_score} "
|
| 639 |
+
f"({len(verdicts)} model verdicts: {[v['verdict'] for v in verdicts]})",
|
| 640 |
+
"ACTS",
|
| 641 |
+
)
|
| 642 |
except Exception as e:
|
| 643 |
hermes_log(f"ACTS consensus failed for {finding.finding_id}: {e}", "WARN")
|
| 644 |
finding.acts_score = finding.confidence
|
requirements.txt
CHANGED
|
@@ -24,7 +24,8 @@ z3-solver>=4.12.0
|
|
| 24 |
qdrant-client>=1.9.0
|
| 25 |
transformers>=4.40.0
|
| 26 |
torch>=2.2.0
|
| 27 |
-
atheris
|
|
|
|
| 28 |
angr>=9.2.0
|
| 29 |
networkx>=3.0
|
| 30 |
defusedxml>=0.7.1
|
|
|
|
| 24 |
qdrant-client>=1.9.0
|
| 25 |
transformers>=4.40.0
|
| 26 |
torch>=2.2.0
|
| 27 |
+
# atheris removed: requires Clang + libFuzzer at compile time which is unavailable
|
| 28 |
+
# on HuggingFace Space Docker images. Fuzzing falls back to Hypothesis automatically.
|
| 29 |
angr>=9.2.0
|
| 30 |
networkx>=3.0
|
| 31 |
defusedxml>=0.7.1
|