Files
Open_Ultra_SAST/src/openultrasast/config.py
T
Marius CiepluchandClaude Opus 5 b57b1a35d8 feat(learning-harness): thinking is a measured condition, and every round
records whether its decision beat noise

Two things the maintainer was right about.

Thinking was disabled so `temperature: 0` would not be silently ignored --
a trade that only pays if it buys determinism. Round zero says it did not:
11 of 20 injection pairs disagreed with themselves across five runs, so the
variance is in the agentic path, not the decoder. We paid the reasoning cost
on a reasoning model and got nothing. It is now `[learning] thinking`,
recorded in the artifact and part of its key so the two modes cannot be read
as one number. The default stays off only because that is what round zero
was measured under.

And the deeper point: the spec built `sign_test` and `reliable_change` and
the acceptance rule calls neither -- `decide` compares raw deltas. On a
detector with that floor a raw delta is mostly noise, and a rule that
brittle is exactly what makes a deterministic decoder look necessary. It is
not; the alternative is an acceptance rule that models the score as noisy,
the way reflective-evolution and Bayesian prompt optimizers do. Changing
Req 9.5 is a maintainer's call, so this does not change the decision: every
round now journals the two-sided sign test over the pairs that moved, and
after four rounds the journal will say whether any decision could be told
from a coin flip.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-06 15:12:09 +02:00

411 lines
14 KiB
Python

from __future__ import annotations
import json
import os
import tomllib
from dataclasses import asdict, dataclass
from pathlib import Path
DEFAULT_VECTOR_STORE = "json-local"
def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
"""Load KEY=VALUE lines from .env into os.environ. Stdlib only; never overrides."""
if not force and (os.environ.get("PYTEST_CURRENT_TEST") or os.environ.get("OPENULTRASAST_SKIP_DOTENV")):
return
env_path = path if path is not None else Path(".env")
if not env_path.is_file():
return
for raw in env_path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip("'").strip('"')
if key and key not in os.environ:
os.environ[key] = value
@dataclass(frozen=True)
class ModelConfig:
ranker: str | None = None
hunter: str | None = None
verifier: str | None = None
patcher: str | None = None
judge: str | None = None # second, independent judgement before anything is published as proven
chat_base_url: str | None = None # chat endpoint; independent of the embedding endpoint
chat_api_key_env: str | None = None # environment variable holding that endpoint's key
@dataclass(frozen=True)
class EmbeddingConfig:
model: str | None = None
store: str = DEFAULT_VECTOR_STORE
@dataclass(frozen=True)
class SandboxConfig:
network: bool = False
workspace_readonly: bool = True
memory_mb: int = 2048
timeout_seconds: int = 300
pids_limit: int = 512
@dataclass(frozen=True)
class DynamicConfig:
enabled: bool = False
network_scope: tuple[str, ...] = ()
@dataclass(frozen=True)
class EvidenceConfig:
minimum_report_verified: str = "static_corroboration"
minimum_exploit: str = "crash_reproduced"
minimum_patch: str = "root_cause_explained"
@dataclass(frozen=True)
class StaticAnalysisConfig:
sarif_paths: tuple[str, ...] = ()
@dataclass(frozen=True)
class ScoreConfig:
k: float = 60.0
min_score: int = 80
block_severity_reachable: int = 5
blocking: bool = False
@dataclass(frozen=True)
class RulesetConfig:
# Emission floors that replace "every regex match becomes a finding". The
# defaults (0.0) emit everything, keeping output byte-identical by default.
min_emit_priority: float = 0.0
min_emit_precision: float = 0.0
@dataclass(frozen=True)
class HarnessxConfig:
# Optional agentic plane (the openultrasast[harnessx] extra). `provider`
# selects the HarnessX LLM provider used by both the hunter pool and the
# llm-judge verifier; the budget caps bound per-task cost and token spend.
# Inert unless the extra is installed and a model is configured in [models].
provider: str = "anthropic"
max_cost_usd: float = 2.0
token_threshold: int = 120_000
@dataclass(frozen=True)
class HardeningConfig:
# Release-readiness controls. redact_secrets masks credentials in traces/reports;
# max_findings caps the reported finding count for bounded CI runs (0 = unlimited).
redact_secrets: bool = True
max_findings: int = 0
@dataclass(frozen=True)
class FusionConfig:
# Two-panel fusion adjudication for triggered findings (standard mode). Runs
# deterministically by default; setting panel_model routes the panels through
# the configured HarnessX provider. high_assurance forces fusion on every finding.
enabled: bool = True
panel_model: str | None = None
decider_model: str | None = None
high_assurance: bool = False
@dataclass(frozen=True)
class ComplexityConfig:
# Stage-2 map size and hunter attention. Small defaults keep PR/nightly map work cheap.
top_k: int = 20
max_hunter_hotspots: int = 8
@dataclass(frozen=True)
class RegressConfig:
# Stage-3 candidate cap and optional per-language sandbox image pins. Isolation limits stay on SandboxConfig.
max_candidates: int = 5
images: tuple[tuple[str, str], ...] = ()
@dataclass(frozen=True)
class VariantsConfig:
# corpus-seeded-mechanisms: structural variant search in MAP (standard/deep only).
enabled: bool = True
max_mechanisms: int = 500
@dataclass(frozen=True)
class LearningConfig:
# learning-harness: the classified detector set and its rounds.
enabled: bool = False # off until round zero has written the family configurations this reads
families_path: str | None = None # taxonomy file; the shipped ruleset when unset
configs_dir: str = ".openultrasast/learning/configs"
k_runs: int = 5 # runs per pair; a single run misses most real changes
round_cost_cap_usd: float = 10.0
minibatch: int = 8 # train pairs in a round's first stage
detector_model: str | None = None
judge_model: str | None = None
cutoff_date: str | None = None # rows fixed after this date are reported as a separate slice
# Whether the detector model reasons before answering. Off by default because that is what round zero was
# measured under, not because it is known to be better: the provider silently ignores `temperature` while
# thinking, and thinking was disabled to make `temperature: 0` mean something. It did not — the measured
# disagreement between runs comes from the agentic path, not the decoder — so which mode wins is a question
# for a second baseline rather than for a default (Req 8.3).
thinking: bool = False
@dataclass(frozen=True)
class ObligationsConfig:
# authorization-obligations: the obligation checker in MAP (standard/deep only).
enabled: bool = True
min_siblings: int = 3
policy_path: str = ".openultrasast/obligations.toml"
@dataclass(frozen=True)
class ResolvedConfig:
models: ModelConfig = ModelConfig()
embeddings: EmbeddingConfig = EmbeddingConfig()
sandbox: SandboxConfig = SandboxConfig()
dynamic: DynamicConfig = DynamicConfig()
evidence: EvidenceConfig = EvidenceConfig()
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
score: ScoreConfig = ScoreConfig()
ruleset: RulesetConfig = RulesetConfig()
harnessx: HarnessxConfig = HarnessxConfig()
fusion: FusionConfig = FusionConfig()
hardening: HardeningConfig = HardeningConfig()
complexity: ComplexityConfig = ComplexityConfig()
regress: RegressConfig = RegressConfig()
variants: VariantsConfig = VariantsConfig()
obligations: ObligationsConfig = ObligationsConfig()
learning: LearningConfig = LearningConfig()
runs_dir: str = ".openultrasast/runs"
def load_config(config_path: Path | None = None) -> ResolvedConfig:
load_dotenv()
data: dict[str, object] = {}
if config_path is not None and config_path.exists():
with config_path.open("rb") as handle:
data = tomllib.load(handle)
return ResolvedConfig(
models=_load_models(data.get("models", {})),
embeddings=_load_embeddings(data.get("embeddings", {})),
sandbox=_load_sandbox(data.get("sandbox", {})),
dynamic=_load_dynamic(data.get("dynamic", {})),
evidence=_load_evidence(data.get("evidence", {})),
static_analysis=_load_static_analysis(data.get("static_analysis", {})),
score=_load_score(data.get("score", {})),
ruleset=_load_ruleset_config(data.get("ruleset", {})),
harnessx=_load_harnessx(data.get("harnessx", {})),
fusion=_load_fusion(data.get("fusion", {})),
hardening=_load_hardening(data.get("hardening", {})),
complexity=_load_complexity(data.get("complexity", {})),
regress=_load_regress(data.get("regress", {})),
variants=_load_variants(data.get("variants", {})),
obligations=_load_obligations(data.get("obligations", {})),
learning=_load_learning(data.get("learning", {})),
runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"),
)
def write_resolved_config(config: ResolvedConfig, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(asdict(config), indent=2, sort_keys=True) + "\n")
def _section(value: object) -> dict[str, object]:
return value if isinstance(value, dict) else {}
def _string(value: object) -> str | None:
return value if isinstance(value, str) and value else None
def _load_models(value: object) -> ModelConfig:
data = _section(value)
return ModelConfig(
ranker=_string(data.get("ranker")),
hunter=_string(data.get("hunter")),
verifier=_string(data.get("verifier")),
patcher=_string(data.get("patcher")),
judge=_string(data.get("judge")),
chat_base_url=_string(data.get("chat_base_url")),
chat_api_key_env=_string(data.get("chat_api_key_env")),
)
def _load_embeddings(value: object) -> EmbeddingConfig:
data = _section(value)
return EmbeddingConfig(
model=_string(data.get("model")) or _string(os.environ.get("OPENROUTER_EMBEDDING_MODEL")),
store=_string(data.get("store")) or DEFAULT_VECTOR_STORE,
)
def _load_sandbox(value: object) -> SandboxConfig:
data = _section(value)
return SandboxConfig(
network=bool(data.get("network", False)),
workspace_readonly=bool(data.get("workspace_readonly", True)),
memory_mb=_int_value(data.get("memory_mb"), 2048),
timeout_seconds=_int_value(data.get("timeout_seconds"), 300),
pids_limit=_int_value(data.get("pids_limit"), 512),
)
def _load_dynamic(value: object) -> DynamicConfig:
data = _section(value)
scope = data.get("network_scope", [])
if not isinstance(scope, list):
scope = []
return DynamicConfig(enabled=bool(data.get("enabled", False)), network_scope=tuple(str(item) for item in scope))
def _load_evidence(value: object) -> EvidenceConfig:
data = _section(value)
return EvidenceConfig(
minimum_report_verified=str(data.get("minimum_report_verified", "static_corroboration")),
minimum_exploit=str(data.get("minimum_exploit", "crash_reproduced")),
minimum_patch=str(data.get("minimum_patch", "root_cause_explained")),
)
def _int_value(value: object, default: int) -> int:
if isinstance(value, bool):
return default
if isinstance(value, int | str | bytes | bytearray):
return int(value)
return default
def _load_static_analysis(value: object) -> StaticAnalysisConfig:
data = _section(value)
paths = data.get("sarif_paths", [])
if not isinstance(paths, list):
paths = []
return StaticAnalysisConfig(sarif_paths=tuple(str(item) for item in paths))
def _load_score(value: object) -> ScoreConfig:
data = _section(value)
return ScoreConfig(
k=_float_value(data.get("k"), 60.0),
min_score=_int_value(data.get("min_score"), 80),
block_severity_reachable=_int_value(data.get("block_severity_reachable"), 5),
blocking=bool(data.get("blocking", False)),
)
def _load_ruleset_config(value: object) -> RulesetConfig:
data = _section(value)
return RulesetConfig(
min_emit_priority=_float_value(data.get("min_emit_priority"), 0.0),
min_emit_precision=_float_value(data.get("min_emit_precision"), 0.0),
)
def _load_harnessx(value: object) -> HarnessxConfig:
data = _section(value)
return HarnessxConfig(
provider=_string(data.get("provider")) or "anthropic",
max_cost_usd=_float_value(data.get("max_cost_usd"), 2.0),
token_threshold=_int_value(data.get("token_threshold"), 120_000),
)
def _load_hardening(value: object) -> HardeningConfig:
data = _section(value)
return HardeningConfig(
redact_secrets=bool(data.get("redact_secrets", True)),
max_findings=_int_value(data.get("max_findings"), 0),
)
def _load_fusion(value: object) -> FusionConfig:
data = _section(value)
return FusionConfig(
enabled=bool(data.get("enabled", True)),
panel_model=_string(data.get("panel_model")),
decider_model=_string(data.get("decider_model")),
high_assurance=bool(data.get("high_assurance", False)),
)
def _load_complexity(value: object) -> ComplexityConfig:
data = _section(value)
return ComplexityConfig(
top_k=_int_value(data.get("top_k"), 20),
max_hunter_hotspots=_int_value(data.get("max_hunter_hotspots"), 8),
)
def _load_regress(value: object) -> RegressConfig:
data = _section(value)
return RegressConfig(
max_candidates=_int_value(data.get("max_candidates"), 5),
images=_string_map(data.get("images")),
)
def _string_map(value: object) -> tuple[tuple[str, str], ...]:
data = _section(value)
images: list[tuple[str, str]] = []
for key, item in data.items():
pinned = _string(item)
if pinned is not None:
images.append((str(key), pinned))
return tuple(sorted(images))
def _float_value(value: object, default: float) -> float:
if isinstance(value, bool):
return default
if isinstance(value, int | float):
return float(value)
if isinstance(value, str):
try:
return float(value)
except ValueError:
return default
return default
def _load_variants(value: object) -> VariantsConfig:
data = _section(value)
return VariantsConfig(
enabled=bool(data.get("enabled", True)),
max_mechanisms=_int_value(data.get("max_mechanisms"), 500),
)
def _load_learning(value: object) -> LearningConfig:
data = _section(value)
return LearningConfig(
enabled=bool(data.get("enabled", LearningConfig.enabled)),
families_path=_string(data.get("families_path")),
configs_dir=_string(data.get("configs_dir")) or LearningConfig.configs_dir,
k_runs=_int_value(data.get("k_runs"), LearningConfig.k_runs),
round_cost_cap_usd=_float_value(data.get("round_cost_cap_usd"), LearningConfig.round_cost_cap_usd),
minibatch=_int_value(data.get("minibatch"), LearningConfig.minibatch),
detector_model=_string(data.get("detector_model")),
judge_model=_string(data.get("judge_model")),
cutoff_date=_string(data.get("cutoff_date")),
thinking=bool(data.get("thinking", False)),
)
def _load_obligations(value: object) -> ObligationsConfig:
data = _section(value)
return ObligationsConfig(
enabled=bool(data.get("enabled", True)),
min_siblings=_int_value(data.get("min_siblings"), 3),
policy_path=str(data.get("policy_path", ".openultrasast/obligations.toml")),
)