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:
Marius Ciepluch
2026-10-02 07:51:38 +02:00
co-authored by Claude Opus 5.5
parent 3091ed42ea
commit bd74ba63b2
6 changed files with 66 additions and 92 deletions
@@ -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.
+16
View File
@@ -1,5 +1,21 @@
# Unreleased # 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 removed; the ax plane is the agentic path (2026-09-30)
HarnessX, the optional agentic extra (`openultrasast[harnessx]`), is removed with its code, HarnessX, the optional agentic extra (`openultrasast[harnessx]`), is removed with its code,
+19 -58
View File
@@ -20,10 +20,24 @@ _PLANE_RUN = "`ousast plane run`, see ops/ax/README.md"
_RETIRED_PANELS = ( _RETIRED_PANELS = (
f"LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ({_PLANE_RUN})" 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] = { RETIRED_KEYS: dict[tuple[str, str], str] = {
("models", "verifier"): f"LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})", ("models", "verifier"): f"{_RETIRED_ON}: LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})",
("fusion", "panel_model"): _RETIRED_PANELS, ("fusion", "panel_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
("fusion", "decider_model"): _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 = "harnessx" # retired 2026-09-30: loads with one warning and is otherwise ignored (Req 3.3)
RETIRED_SECTION_WARNING = ( RETIRED_SECTION_WARNING = (
@@ -38,10 +52,10 @@ class RetiredConfigError(ValueError):
def _check_retired(data: dict[str, object]) -> None: def _check_retired(data: dict[str, object]) -> None:
problems = [] problems = []
for (section, key), replacement in RETIRED_KEYS.items(): for (section, key), reason in RETIRED_KEYS.items():
table = data.get(section) table = data.get(section)
if isinstance(table, dict) and key in table: 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: if problems:
raise RetiredConfigError("\n".join(problems)) raise RetiredConfigError("\n".join(problems))
if RETIRED_SECTION in data: if RETIRED_SECTION in data:
@@ -70,7 +84,6 @@ def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
class ModelConfig: class ModelConfig:
ranker: str | None = None ranker: str | None = None
hunter: str | None = None hunter: str | None = None
patcher: str | None = None
judge: str | None = None # second, independent judgement before anything is published as proven 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_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 chat_api_key_env: str | None = None # environment variable holding that endpoint's key
@@ -96,19 +109,6 @@ class SandboxConfig:
pids_limit: int = 512 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) @dataclass(frozen=True)
class StaticAnalysisConfig: class StaticAnalysisConfig:
sarif_paths: tuple[str, ...] = () sarif_paths: tuple[str, ...] = ()
@@ -160,13 +160,6 @@ class RegressConfig:
images: tuple[tuple[str, str], ...] = () 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) @dataclass(frozen=True)
class ModelLayerConfig: class ModelLayerConfig:
# contributor-scan: the model layer in MAP. Named for the LAYER, not the model: `ModelConfig` is already # 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() models: ModelConfig = ModelConfig()
embeddings: EmbeddingConfig = EmbeddingConfig() embeddings: EmbeddingConfig = EmbeddingConfig()
sandbox: SandboxConfig = SandboxConfig() sandbox: SandboxConfig = SandboxConfig()
dynamic: DynamicConfig = DynamicConfig()
evidence: EvidenceConfig = EvidenceConfig()
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig() static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
score: ScoreConfig = ScoreConfig() score: ScoreConfig = ScoreConfig()
ruleset: RulesetConfig = RulesetConfig() ruleset: RulesetConfig = RulesetConfig()
@@ -223,7 +214,6 @@ class ResolvedConfig:
hardening: HardeningConfig = HardeningConfig() hardening: HardeningConfig = HardeningConfig()
complexity: ComplexityConfig = ComplexityConfig() complexity: ComplexityConfig = ComplexityConfig()
regress: RegressConfig = RegressConfig() regress: RegressConfig = RegressConfig()
variants: VariantsConfig = VariantsConfig()
obligations: ObligationsConfig = ObligationsConfig() obligations: ObligationsConfig = ObligationsConfig()
model: ModelLayerConfig = ModelLayerConfig() model: ModelLayerConfig = ModelLayerConfig()
push: PushConfig = PushConfig() 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", {})), models=_load_models(data.get("models", {})),
embeddings=_load_embeddings(data.get("embeddings", {})), embeddings=_load_embeddings(data.get("embeddings", {})),
sandbox=_load_sandbox(data.get("sandbox", {})), 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", {})), static_analysis=_load_static_analysis(data.get("static_analysis", {})),
score=_load_score(data.get("score", {})), score=_load_score(data.get("score", {})),
ruleset=_load_ruleset_config(data.get("ruleset", {})), 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", {})), hardening=_load_hardening(data.get("hardening", {})),
complexity=_load_complexity(data.get("complexity", {})), complexity=_load_complexity(data.get("complexity", {})),
regress=_load_regress(data.get("regress", {})), regress=_load_regress(data.get("regress", {})),
variants=_load_variants(data.get("variants", {})),
obligations=_load_obligations(data.get("obligations", {})), obligations=_load_obligations(data.get("obligations", {})),
model=_load_model(data.get("model", {})), model=_load_model(data.get("model", {})),
push=PushConfig.from_payload(data.get("push", {})), push=PushConfig.from_payload(data.get("push", {})),
@@ -283,7 +270,6 @@ def _load_models(value: object) -> ModelConfig:
return ModelConfig( return ModelConfig(
ranker=_string(data.get("ranker")), ranker=_string(data.get("ranker")),
hunter=_string(data.get("hunter")), hunter=_string(data.get("hunter")),
patcher=_string(data.get("patcher")),
judge=_string(data.get("judge")), judge=_string(data.get("judge")),
chat_base_url=_string(data.get("chat_base_url")), chat_base_url=_string(data.get("chat_base_url")),
chat_api_key_env=_string(data.get("chat_api_key_env")), 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: def _int_value(value: object, default: int) -> int:
if isinstance(value, bool): if isinstance(value, bool):
return default return default
@@ -416,14 +385,6 @@ def _float_value(value: object, default: float) -> float:
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: def _load_model(value: object) -> ModelLayerConfig:
data = _section(value) data = _section(value)
return ModelLayerConfig( return ModelLayerConfig(
+1 -1
View File
@@ -181,7 +181,7 @@ def write_harness_config(
"ranker": config.models.ranker, "ranker": config.models.ranker,
"hunter": config.models.hunter, "hunter": config.models.hunter,
"verifier": None, # schema kept byte for byte; LLM verification runs on the plane "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}, 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())}, prompt_hashes={name: _hash_text(prompt) for name, prompt in sorted(prompts.items())},
+30 -6
View File
@@ -1,3 +1,4 @@
import json
import logging import logging
import os import os
from dataclasses import replace from dataclasses import replace
@@ -17,9 +18,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
'ranker = "openrouter/test-ranker"', 'ranker = "openrouter/test-ranker"',
"[sandbox]", "[sandbox]",
"memory_mb = 1024", "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.models.ranker == "openrouter/test-ranker"
assert config.embeddings.store == "json-local" assert config.embeddings.store == "json-local"
assert config.sandbox.memory_mb == 1024 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: 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 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: def test_the_cli_exits_2_on_a_retired_key(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
from openultrasast.cli import main 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) write_resolved_config(load_config(None), output)
assert output.exists() 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: def test_complexity_and_regress_defaults_when_section_absent() -> None:
-1
View File
@@ -9,7 +9,6 @@ def test_project_opencode_skills_have_required_frontmatter() -> None:
assert {path.parent.name for path in skill_files} == { assert {path.parent.name for path in skill_files} == {
"openultrasast-fix-audit", "openultrasast-fix-audit",
"openultrasast-kiro-impl",
"openultrasast-scan", "openultrasast-scan",
"openultrasast-triage", "openultrasast-triage",
} }