from __future__ import annotations import json import os import tomllib from dataclasses import asdict, dataclass from pathlib import Path from typing import Literal from openultrasast.contracts import Contract 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 # Whether the chat model reasons before answering. Off by default because that is what the committed # baseline was measured under: the provider silently ignores `temperature` while thinking, so thinking was # disabled to make `temperature: 0` mean something. It did not — the measured run-to-run disagreement came # from the agentic path, not the decoder — which is why the model layer arbitrates instead of averaging. thinking: bool = False @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 ModelLayerConfig: # contributor-scan: the model layer in MAP. Named for the LAYER, not the model: `ModelConfig` is already # the `[models]` block (hunter, judge, endpoint) and two meanings of one name is how you get a scan that # silently reads the wrong settings. Additive -- without a CPG engine the scan is unchanged. enabled: bool = True # A TOTAL for the run, not a per-region cap. Conservative by default: a first scan of an unfamiliar # repository should not be able to run away with someone's budget before they have decided they want it. max_model_calls: int = 100 max_regions: int = 300 @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 PushConfig(Contract): """Inert experimental push settings; configuring them does not install a hook. The 2 GiB local cache cap is a storage budget, not a performance promise. The eventual cache evicts unleased entries; deadline and cleanup are separate costs. No endpoint, fetch, engine download or capability admission is enabled here. """ comparison_base: str | None = None deadline_seconds: float = 30.0 cancellation_allowance_seconds: float = 2.0 cache_max_bytes: int = 2 * 1024**3 mode: Literal["advisory", "blocking"] = "advisory" incomplete_coverage_policy: Literal["allow", "block"] = "allow" def __post_init__(self) -> None: super().__post_init__() if self.deadline_seconds <= 0 or self.cancellation_allowance_seconds <= 0 or self.cache_max_bytes <= 0: raise ValueError("push deadlines, cancellation allowance and cache limit must be positive") @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() model: ModelLayerConfig = ModelLayerConfig() push: PushConfig = PushConfig() runs_dir: str = ".openultrasast/runs" def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> ResolvedConfig: if dotenv: 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", {})), model=_load_model(data.get("model", {})), push=PushConfig.from_payload(data.get("push", {})), runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"), ) def config_payload(config: ResolvedConfig) -> dict[str, object]: """Reproducible resolved settings, including the separate experimental push policy.""" return asdict(config) def write_resolved_config(config: ResolvedConfig, path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(config_payload(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")), thinking=bool(data.get("thinking", False)), ) 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_model(value: object) -> ModelLayerConfig: data = _section(value) return ModelLayerConfig( enabled=bool(data.get("enabled", ModelLayerConfig.enabled)), max_model_calls=_int_value(data.get("max_model_calls"), ModelLayerConfig.max_model_calls), max_regions=_int_value(data.get("max_regions"), ModelLayerConfig.max_regions), ) 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")), )