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
## 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
View File
@@ -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(
+1 -1
View File
@@ -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
View File
@@ -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:
-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} == {
"openultrasast-fix-audit",
"openultrasast-kiro-impl",
"openultrasast-scan",
"openultrasast-triage",
}