mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
chore(config): retire unread keys [dynamic], [evidence], [variants], [models] patcher
Nothing in src reads config.dynamic, config.evidence or config.variants, and
[models] patcher was only echoed into harness.json. They now go through the
existing RETIRED_KEYS mechanism and fail with RetiredConfigError naming why
("retired 2026-10-02 because nothing ever read it: ..."); RETIRED_KEYS now
carries the full reason per key so the HarnessX entries keep their wording.
- DynamicConfig, EvidenceConfig, VariantsConfig and their loaders removed;
ModelConfig.patcher removed. resolved_config.json loses those sections.
- harness.json keeps its shape: model_roles.patcher is always null.
- RELEASE_NOTES "Unreleased" lists the keys and the output shape change.
- Delete .opencode/skills/openultrasast-kiro-impl: it pointed agents at the
retired openrouter-sast-harness tasks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3091ed42ea
commit
bd74ba63b2
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: openultrasast-kiro-impl
|
||||
description: OpenUltraSAST Kiro implementation steering. Use when continuing `.kiro/specs/openrouter-sast-harness/tasks.md`, choosing the next phase, updating checkboxes, or keeping implementation aligned with the spec.
|
||||
---
|
||||
|
||||
# OpenUltraSAST Kiro Implementation
|
||||
|
||||
Use this skill when implementing the OpenUltraSAST Kiro spec from opencode.
|
||||
|
||||
## Rules
|
||||
|
||||
1. Read `.kiro/specs/openrouter-sast-harness/tasks.md` before choosing work.
|
||||
2. Implement the next unchecked task slice unless the user names a different phase.
|
||||
3. Keep each slice independently verifiable with `uv run pytest`.
|
||||
4. Update task checkboxes only after implementation and tests pass.
|
||||
5. Prefer minimal harness-compatible primitives over large framework imports.
|
||||
6. Keep OpenUltraSAST independent of Clearwing; use Clearwing only as an oracle for design patterns.
|
||||
7. Use project-local opencode skills to steer scan, triage, and fix workflows.
|
||||
|
||||
## Current Phase Order
|
||||
|
||||
- Phase 5: Harness runtime events, processors, contracts, and traces.
|
||||
- Phase 5A: Static mapping disciplines for SARIF, Semgrep, CodeQL, differential review, and sharp edges.
|
||||
- Phase 6: Evidence ladder verifier.
|
||||
- Phase 7 and 7A: Docker sandbox and dynamic evidence.
|
||||
- Phase 8 and later: Hunter pool, SARIF, deep mode, patch oracle, MCP, fusion, skill index, hardening.
|
||||
@@ -1,5 +1,21 @@
|
||||
# Unreleased
|
||||
|
||||
## Unread config keys retired (2026-10-02)
|
||||
|
||||
These keys loaded but nothing ever read them. They now fail with `RetiredConfigError` (the
|
||||
CLI prints it and exits 2), naming why. Remove them from your config:
|
||||
|
||||
- `[models] patcher`: no patching stage calls a model.
|
||||
- `[dynamic] enabled`, `[dynamic] network_scope`: no dynamic or deep tier exists yet.
|
||||
- `[evidence] minimum_report_verified`, `minimum_exploit`, `minimum_patch`: evidence tiers
|
||||
come from the program model's ladder, not from configuration.
|
||||
- `[variants] enabled`, `[variants] max_mechanisms`: the variant search they configured was
|
||||
deleted earlier.
|
||||
|
||||
**Output shape.** `resolved_config.json` no longer has the `dynamic`, `evidence` and
|
||||
`variants` sections or `models.patcher`. `harness.json` keeps its shape:
|
||||
`model_roles.patcher` is always `null`, like `model_roles.verifier`.
|
||||
|
||||
## HarnessX removed; the ax plane is the agentic path (2026-09-30)
|
||||
|
||||
HarnessX, the optional agentic extra (`openultrasast[harnessx]`), is removed with its code,
|
||||
|
||||
+19
-58
@@ -20,10 +20,24 @@ _PLANE_RUN = "`ousast plane run`, see ops/ax/README.md"
|
||||
_RETIRED_PANELS = (
|
||||
f"LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ({_PLANE_RUN})"
|
||||
)
|
||||
# Keys that loaded but were never read (retired 2026-10-02, legacy cleanup): accepting them silently would let
|
||||
# a user believe they configure something.
|
||||
_UNREAD_ON = "retired 2026-10-02 because nothing ever read it"
|
||||
_UNREAD_DYNAMIC = "no dynamic or deep tier exists; deep mode will define its own keys when it is built"
|
||||
_UNREAD_EVIDENCE = "evidence tiers come from the program model's ladder (model/ladder.py), not from configuration"
|
||||
_UNREAD_VARIANTS = "the structural variant search it configured was deleted with semantic/variants.py"
|
||||
RETIRED_KEYS: dict[tuple[str, str], str] = {
|
||||
("models", "verifier"): f"LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})",
|
||||
("fusion", "panel_model"): _RETIRED_PANELS,
|
||||
("fusion", "decider_model"): _RETIRED_PANELS,
|
||||
("models", "verifier"): f"{_RETIRED_ON}: LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})",
|
||||
("fusion", "panel_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
||||
("fusion", "decider_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
||||
("models", "patcher"): f"{_UNREAD_ON}: no patching stage calls a model",
|
||||
("dynamic", "enabled"): f"{_UNREAD_ON}: {_UNREAD_DYNAMIC}",
|
||||
("dynamic", "network_scope"): f"{_UNREAD_ON}: {_UNREAD_DYNAMIC}",
|
||||
("evidence", "minimum_report_verified"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||
("evidence", "minimum_exploit"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||
("evidence", "minimum_patch"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||
("variants", "enabled"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||
("variants", "max_mechanisms"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||
}
|
||||
RETIRED_SECTION = "harnessx" # retired 2026-09-30: loads with one warning and is otherwise ignored (Req 3.3)
|
||||
RETIRED_SECTION_WARNING = (
|
||||
@@ -38,10 +52,10 @@ class RetiredConfigError(ValueError):
|
||||
|
||||
def _check_retired(data: dict[str, object]) -> None:
|
||||
problems = []
|
||||
for (section, key), replacement in RETIRED_KEYS.items():
|
||||
for (section, key), reason in RETIRED_KEYS.items():
|
||||
table = data.get(section)
|
||||
if isinstance(table, dict) and key in table:
|
||||
problems.append(f"`[{section}] {key}` was {_RETIRED_ON}: {replacement}. Remove the key.")
|
||||
problems.append(f"`[{section}] {key}` was {reason}. Remove the key.")
|
||||
if problems:
|
||||
raise RetiredConfigError("\n".join(problems))
|
||||
if RETIRED_SECTION in data:
|
||||
@@ -70,7 +84,6 @@ def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
|
||||
class ModelConfig:
|
||||
ranker: str | None = None
|
||||
hunter: 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
|
||||
@@ -96,19 +109,6 @@ class SandboxConfig:
|
||||
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, ...] = ()
|
||||
@@ -160,13 +160,6 @@ class RegressConfig:
|
||||
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
|
||||
@@ -214,8 +207,6 @@ 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()
|
||||
@@ -223,7 +214,6 @@ class ResolvedConfig:
|
||||
hardening: HardeningConfig = HardeningConfig()
|
||||
complexity: ComplexityConfig = ComplexityConfig()
|
||||
regress: RegressConfig = RegressConfig()
|
||||
variants: VariantsConfig = VariantsConfig()
|
||||
obligations: ObligationsConfig = ObligationsConfig()
|
||||
model: ModelLayerConfig = ModelLayerConfig()
|
||||
push: PushConfig = PushConfig()
|
||||
@@ -243,8 +233,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
|
||||
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", {})),
|
||||
@@ -252,7 +240,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
|
||||
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", {})),
|
||||
@@ -283,7 +270,6 @@ def _load_models(value: object) -> ModelConfig:
|
||||
return ModelConfig(
|
||||
ranker=_string(data.get("ranker")),
|
||||
hunter=_string(data.get("hunter")),
|
||||
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")),
|
||||
@@ -310,23 +296,6 @@ def _load_sandbox(value: object) -> SandboxConfig:
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -416,14 +385,6 @@ def _float_value(value: object, default: float) -> float:
|
||||
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(
|
||||
|
||||
@@ -181,7 +181,7 @@ def write_harness_config(
|
||||
"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,
|
||||
"patcher": None, # schema kept; `[models] patcher` was retired 2026-10-02 (never read)
|
||||
},
|
||||
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())},
|
||||
|
||||
+30
-6
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import replace
|
||||
@@ -17,9 +18,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
|
||||
'ranker = "openrouter/test-ranker"',
|
||||
"[sandbox]",
|
||||
"memory_mb = 1024",
|
||||
"[dynamic]",
|
||||
"enabled = true",
|
||||
'network_scope = ["127.0.0.1:8080"]',
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -29,8 +27,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
|
||||
assert config.models.ranker == "openrouter/test-ranker"
|
||||
assert config.embeddings.store == "json-local"
|
||||
assert config.sandbox.memory_mb == 1024
|
||||
assert config.dynamic.enabled is True
|
||||
assert config.dynamic.network_scope == ("127.0.0.1:8080",)
|
||||
|
||||
|
||||
def test_retired_agentic_section_loads_with_one_warning(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||
@@ -67,6 +63,31 @@ def test_retired_llm_keys_fail_naming_the_replacement(tmp_path: Path, body: str,
|
||||
assert key in message and replacement in message and "ousast plane run" in message and "Remove the key" in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "key"),
|
||||
[
|
||||
('[models]\npatcher = "gpt-4o"\n', "`[models] patcher`"),
|
||||
("[dynamic]\nenabled = true\n", "`[dynamic] enabled`"),
|
||||
('[dynamic]\nnetwork_scope = ["127.0.0.1:8080"]\n', "`[dynamic] network_scope`"),
|
||||
('[evidence]\nminimum_report_verified = "static_corroboration"\n', "`[evidence] minimum_report_verified`"),
|
||||
('[evidence]\nminimum_exploit = "crash_reproduced"\n', "`[evidence] minimum_exploit`"),
|
||||
('[evidence]\nminimum_patch = "root_cause_explained"\n', "`[evidence] minimum_patch`"),
|
||||
("[variants]\nenabled = false\n", "`[variants] enabled`"),
|
||||
("[variants]\nmax_mechanisms = 10\n", "`[variants] max_mechanisms`"),
|
||||
],
|
||||
)
|
||||
def test_unread_keys_fail_naming_why(tmp_path: Path, body: str, key: str) -> None:
|
||||
"""Keys that loaded but were never read are retired (2026-10-02) the same way, each naming its reason."""
|
||||
config_path = tmp_path / "openultrasast.toml"
|
||||
config_path.write_text(body)
|
||||
|
||||
with pytest.raises(RetiredConfigError) as raised:
|
||||
load_config(config_path)
|
||||
|
||||
message = str(raised.value)
|
||||
assert key in message and "retired 2026-10-02 because nothing ever read it" in message and "Remove the key" in message
|
||||
|
||||
|
||||
def test_the_cli_exits_2_on_a_retired_key(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
from openultrasast.cli import main
|
||||
|
||||
@@ -116,7 +137,10 @@ def test_write_resolved_config_creates_json(tmp_path: Path) -> None:
|
||||
write_resolved_config(load_config(None), output)
|
||||
|
||||
assert output.exists()
|
||||
assert '"minimum_report_verified": "static_corroboration"' in output.read_text()
|
||||
payload = json.loads(output.read_text())
|
||||
assert payload["sandbox"]["memory_mb"] == 2048
|
||||
# retired 2026-10-02 (never read): the unread sections are not echoed as if they configured something
|
||||
assert not {"dynamic", "evidence", "variants"} & set(payload) and "patcher" not in payload["models"]
|
||||
|
||||
|
||||
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
||||
|
||||
@@ -9,7 +9,6 @@ def test_project_opencode_skills_have_required_frontmatter() -> None:
|
||||
|
||||
assert {path.parent.name for path in skill_files} == {
|
||||
"openultrasast-fix-audit",
|
||||
"openultrasast-kiro-impl",
|
||||
"openultrasast-scan",
|
||||
"openultrasast-triage",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user