mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
The standard scan now runs run_hunter_pool, verify_findings and the deterministic fuse_findings directly; the HarnessX hunter orchestrator, the LLM judge dispatcher (verify_judge.py, deleted with its test) and the LLM fusion panels are gone, and so are their extra-absent degradations. host_under_harnessx is dropped from stage_processors. config: [models] verifier and [fusion] panel_model/decider_model raise RetiredConfigError naming the plane replacement (the CLI prints it and exits 2); a [harnessx] section loads with one warning and is otherwise ignored. harness.json keeps "verifier": null. Tests that asserted the fallback are renamed to the behaviour they assert; the module-audit manifest drops verify_judge. Nothing outside the two modules deleted in task 6 imports harness_ext or hunter_harness. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections.abc import Callable
|
|
from dataclasses import asdict, dataclass, field
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any, Literal, Protocol
|
|
|
|
from .config import ResolvedConfig
|
|
|
|
EventType = Literal[
|
|
"task_start",
|
|
"stage_start",
|
|
"before_model",
|
|
"after_model",
|
|
"before_tool",
|
|
"after_tool",
|
|
"stage_end",
|
|
"task_end",
|
|
]
|
|
|
|
ContractMode = Literal["strict", "warn"]
|
|
|
|
|
|
class HarnessContractError(ValueError):
|
|
"""Raised when a processor violates its declared state contract."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HarnessEvent:
|
|
event_type: EventType
|
|
scan_id: str
|
|
stage: str | None
|
|
timestamp: str
|
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProcessorSpec:
|
|
name: str
|
|
version: str
|
|
handles: tuple[EventType, ...]
|
|
reads: tuple[str, ...] = ()
|
|
writes: tuple[str, ...] = ()
|
|
|
|
|
|
class Processor(Protocol):
|
|
spec: ProcessorSpec
|
|
|
|
def handle(self, event: HarnessEvent, state: dict[str, Any]) -> dict[str, Any] | None:
|
|
raise NotImplementedError
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SerializedHarnessConfig:
|
|
contract_mode: ContractMode
|
|
model_roles: dict[str, str | None]
|
|
processor_versions: dict[str, str]
|
|
prompt_hashes: dict[str, str]
|
|
|
|
|
|
class HarnessTraceWriter:
|
|
def __init__(self, path: Path, *, redact: bool = True) -> None:
|
|
self.path = path
|
|
self.redact = redact
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
def append(self, event: HarnessEvent) -> None:
|
|
line = json.dumps(asdict(event), sort_keys=True)
|
|
if self.redact:
|
|
from .redaction import redact_secrets
|
|
|
|
line = redact_secrets(line)
|
|
with self.path.open("a") as handle:
|
|
handle.write(line + "\n")
|
|
|
|
|
|
class HarnessRuntime:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
scan_id: str,
|
|
config: ResolvedConfig,
|
|
trace_writer: HarnessTraceWriter,
|
|
processors: list[Processor] | None = None,
|
|
contract_mode: ContractMode = "strict",
|
|
) -> None:
|
|
self.scan_id = scan_id
|
|
self.config = config
|
|
self.trace_writer = trace_writer
|
|
self.processors = processors or []
|
|
self.contract_mode = contract_mode
|
|
self.state: dict[str, Any] = {"degradations": []}
|
|
|
|
def start(self, *, mode: str, target: Path) -> None:
|
|
self.emit("task_start", None, {"mode": mode, "target": str(target)})
|
|
|
|
def finish(self, *, status: str) -> None:
|
|
self.emit("task_end", None, {"status": status})
|
|
|
|
def run_stage(self, name: str, callback: Callable[[], Any]) -> Any:
|
|
self.emit("stage_start", name, {})
|
|
try:
|
|
result = callback()
|
|
except Exception as exc:
|
|
self.emit("stage_end", name, {"status": "failed", "error": f"{type(exc).__name__}: {exc}"})
|
|
raise
|
|
self.emit("stage_end", name, {"status": "succeeded"})
|
|
return result
|
|
|
|
def emit(self, event_type: EventType, stage: str | None, payload: dict[str, Any]) -> None:
|
|
event = HarnessEvent(
|
|
event_type=event_type,
|
|
scan_id=self.scan_id,
|
|
stage=stage,
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
payload=payload,
|
|
)
|
|
self.trace_writer.append(event)
|
|
for processor in self.processors:
|
|
if event_type in processor.spec.handles:
|
|
self._run_processor(processor, event)
|
|
|
|
def _run_processor(self, processor: Processor, event: HarnessEvent) -> None:
|
|
before = set(self.state)
|
|
missing_reads = [key for key in processor.spec.reads if key not in self.state]
|
|
if missing_reads:
|
|
self._contract_violation(processor.spec.name, f"missing reads: {', '.join(missing_reads)}")
|
|
return
|
|
|
|
updates = processor.handle(event, dict(self.state)) or {}
|
|
if not isinstance(updates, dict):
|
|
self._contract_violation(processor.spec.name, "processor returned a non-dict update")
|
|
return
|
|
undeclared = sorted(set(updates) - set(processor.spec.writes))
|
|
if undeclared:
|
|
self._contract_violation(processor.spec.name, f"undeclared writes: {', '.join(undeclared)}")
|
|
return
|
|
self.state.update(updates)
|
|
changed = sorted(set(self.state) - before | set(updates))
|
|
self.trace_writer.append(
|
|
HarnessEvent(
|
|
event_type=event.event_type,
|
|
scan_id=self.scan_id,
|
|
stage=event.stage,
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
payload={"processor": processor.spec.name, "changed_state": changed},
|
|
)
|
|
)
|
|
|
|
def _contract_violation(self, processor_name: str, reason: str) -> None:
|
|
message = f"processor {processor_name} contract violation: {reason}"
|
|
if self.contract_mode == "strict":
|
|
raise HarnessContractError(message)
|
|
self.state.setdefault("degradations", []).append(message)
|
|
self.trace_writer.append(
|
|
HarnessEvent(
|
|
event_type="stage_end",
|
|
scan_id=self.scan_id,
|
|
stage="processor_contract",
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
payload={"status": "degraded", "reason": message},
|
|
)
|
|
)
|
|
|
|
|
|
def write_harness_config(
|
|
*,
|
|
config: ResolvedConfig,
|
|
processors: list[Processor],
|
|
contract_mode: ContractMode,
|
|
path: Path,
|
|
prompts: dict[str, str] | None = None,
|
|
) -> None:
|
|
prompts = prompts or {}
|
|
serialized = SerializedHarnessConfig(
|
|
contract_mode=contract_mode,
|
|
model_roles={
|
|
"ranker": config.models.ranker,
|
|
"hunter": config.models.hunter,
|
|
"verifier": None, # schema kept byte for byte; LLM verification runs on the plane
|
|
"patcher": config.models.patcher,
|
|
},
|
|
processor_versions={processor.spec.name: processor.spec.version for processor in processors},
|
|
prompt_hashes={name: _hash_text(prompt) for name, prompt in sorted(prompts.items())},
|
|
)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(asdict(serialized), indent=2, sort_keys=True) + "\n")
|
|
|
|
|
|
def _hash_text(text: str) -> str:
|
|
return hashlib.sha256(text.encode()).hexdigest()
|