README: "Configuring the HarnessX agentic plane" becomes "The agentic plane (ax)" (ousast plane run|status|doctor|remember, per-task Model, budget and egress, pointer to ops/ax/README.md, one tagged retirement line); "The HarnessX self-improving cycle" becomes "The self-improving cycle", and its MetaAgent paragraph is replaced by the memory proposer (`ousast improve --memory`, provenance, train-on-test guard). The fusion panel_model/decider_model lines and the [harnessx] spend caps are gone. The README reference test now passes without its strict xfail. docs/examples.md section 6 runs the plane; docs/threat-model.md describes the per-task EgressPolicy, the usd/calls budgets and the start-request credential path. RELEASE_NOTES.md gets an Unreleased entry: HarnessX removed, the retired keys and their errors, the silent loss of the second hunter for users who set [models] hunter, and the plane as the agentic path. Dated retirement notes (2026-09-30) head the requirements, design and brief of harnessx-self-improving-rulesets, learning-harness, three-stage-scan and openrouter-sast-harness, and follow the spec table in steering/overview.md; history is unchanged. All tasks are checked, so spec.json moves to implementation-complete. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
44 KiB
Design Document
Retired 2026-09-30: HarnessX was removed by
harnessx-removal; the agentic plane is ax (ai-service-plane), and evolutionary proposals come from the plane memory. History below is unchanged.
Overview
Purpose. This feature splits OpenUltraSAST's single overloaded regex layer — which today is detector, severity authority, and volume knob at once — into three governed owners: rules detect, a central CWE policy decides severity and scope, and a single project score becomes the optimization target. On top of that governance plane it adopts the HarnessX runtime exactly where it pays off — the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop that adapts rules and policy gates under hard bounds.
Users. OpenUltraSAST maintainers gain a staged, reversible HarnessX adoption and an automatic, gated rule/policy tuning loop. Security policy owners gain a versioned, central CWE-to-severity policy. Security reviewers and CI consumers gain a bounded 0-100 AppSec project score with a two-condition gate.
Impact. Today findings.PatternRule carries a string severity, quick_scan_findings reads a module-level PATTERN_RULES, calibration.py only demotes ranking (floored at MIN_CALIBRATED_PRIORITY = 0.1, keyed on tags[0]), benchmark.py:368 writes an inert next_improvement_candidate="rules_or_language_hunter", and harness.HarnessRuntime.run_stage is a sync dict-state driver. This design turns rules into TOML data, removes PatternRule.severity, sources severity from a vendored verycode CWE_Score.tsv, computes a project score, makes the benchmark actionable, and confines HarnessX behind an optional, SHA-pinned, lazy-imported extra — while dependencies = [] and requires-python = ">=3.11" stay intact and the ≥90% recall / <10% FP detection gate holds at every phase.
Goals
- Rules become versioned data (
ruleset/<lang>/*.toml) with a loop-controlledstatusand no rule-local severity. - One CWE = one governed severity, sourced from
policy/CWE_Score.tsvviaresolve_severity(). - A single 0-100
project_scorefrompolicy.severity × reachability, with a two-condition CI gate. - HarnessX adopted at the hunter loop, the verifier, and the meta-harness, behind
openultrasast[harnessx]. - A bounded self-improvement loop (
rule,policylevers) that auto-reverts byte-for-byte under the gate. - The governance + scoring + benchmark planes stay zero-dependency and byte-identical by default through Phases 0-2.
Non-Goals
- The loop authoring or rewriting detection pattern text (human PR only).
- The loop editing the authoritative 0-5 CWE severity (verycode-upstream authority).
- A full single-step HarnessX rewrite of all deterministic stages and weight-level RL (deferred to optional Phase 5+).
- Changing the reachability vocabulary, which stays exactly
reachable | inferred-file-surface | unknown. - Dynamic/DAST scoring — report-only for a static tool.
Boundary Commitments
This Spec Owns
- The RulesetStore (
ruleset/store.py+ruleset/<lang>/*.toml):load_ruleset(), thePatternRuleschema (detection-only), and theenabled|shadow|disabledstatus vocabulary. - The PolicyStore (
policy/verycode.py+policy/CWE_Score.tsv):load_policy(),CwePolicy,resolve_severity(), and the CWE-120→CWE-121 remap. - The ScoreModel (
scoring/project_score.py):SEV_WEIGHT,REACH_MULT,finding_penalty,project_score, the score artifact, and the two-condition CI gate. - The loop ledger (
.openultrasast/calibration/rule_policy.json): the loop-owned overlay ofstatus/evidence-floor/threshold/precision. - The HarnessX seam:
harness/build_sast.py,HxScanOrchestrator, theMultiHookProcessorSAST processors, theSlotContractMixin, and theimprove/meta-harness driver. - The benchmark→signal conversion:
rule_idattribution, per-rule precision/recall,rule_policy_delta, andrule_signals.json.
Out of Boundary
- The detection-pattern content of rules (authored by humans via PR; the loop may only toggle status / raise floors).
- The authoritative 0-5 severity numbers in
CWE_Score.tsv(verycode-upstream; PR gaps there). - Upstream HarnessX internals beyond the minimal meta-harness patch (
_VALID_LEVERS,_render_task_brief,EvolveValidator, changeset buckets,contract_autocheck). - The DAST/dynamic execution engine; dynamic-only CWEs are report-only here.
- Retiring
HarnessRuntimeand adoptingrl/— explicitly Phase 5+, optional.
Allowed Dependencies
- Stdlib only for the governance/scoring/benchmark planes:
tomllib,csv,json,math,dataclasses. - HarnessX only behind the
[harnessx]extra, pinned to a commit SHA, imported lazily behind_has_harnessx(). - The existing
HarnessRuntimeas the top-level sync driver through Phases 0-4 (not removed by this spec until Phase 5). - The existing reachability vocabulary emitted by
findings.py(consumed, never redefined).
Revalidation Triggers
- Contract shape changes to
PatternRule(removingseverity),CwePolicy,VerificationResult,BenchmarkMiss/BenchmarkFalsePositive(addingrule_id), or theproject_scoreartifact JSON. - Data ownership change: severity moving from rule → policy; calibration re-keying from
tags[0]→rule_id. - Dependency direction change: any new import of HarnessX outside a lazy
_has_harnessx()guard; any addition to coredependencies. - Runtime prerequisite change: the startup CWE-resolution invariant (fail-loud if an enabled rule's CWE is unmapped); the
pip install --dry-runaudit gate before Phase 3. - Lever surface change: any addition to
journal._VALID_LEVERSor theEvolveValidatorbounds.
Architecture
Existing Architecture Analysis
| Existing element | File | Constraint to respect / debt to fix |
|---|---|---|
HarnessRuntime.run_stage(name, callback) |
harness.py:74 |
Sync, dict state ({"degradations": []}), single HarnessEvent (scan_id), ProcessorSpec.reads/writes allow-list. Incompatible with HarnessX async-generator + message-window contract — bridge, do not map. Stays as the top-level driver through Phase 4. |
PatternRule.severity: str + module-level PATTERN_RULES |
findings.py:31,34 |
The fused defect: regex carries its own severity string ("high"/"medium"/"low") and is the only volume control. Remove severity; externalize patterns to TOML. |
quick_scan_findings(root, targets, rankings) |
findings.py / cli.py:126 |
Reads PATTERN_RULES implicitly. Becomes quick_scan_findings(..., ruleset) wired like static_hints. |
calibrate_rankings demotion |
calibration.py:146 |
max(MIN_CALIBRATED_PRIORITY, calibrated - 0.5), keyed on finding.tags[0] (calibration.py:262). Demotes ranking, never the rule. Re-key to rule_id; write status flips to ledger. |
verify_findings rejects on reachability_status == "unknown" |
verification.py:91 |
Structural verifier. Keep as zero-dep fallback; add LLM-judge path. |
benchmark.py:368 next_improvement_candidate="rules_or_language_hunter" |
benchmark.py |
Inert stub written to calibration_records.json. Make actionable via rule_id attribution + rule_policy_delta. |
cli.py run_stage(...) call chain |
cli.py:104-145 |
The 13-stage sync pipeline. Stages migrate to HarnessX hooks incrementally; the hunter pool first. |
Architecture Pattern & Boundary Map
Selected pattern. Three planes with one-directional authority: governance data drives execution; execution emits signals; the improvement loop edits governance under a gate. Nothing in execution mutates governance directly. The governance + scoring planes are pure stdlib, zero HarnessX (non-negotiable, so CI gating runs in the default install). HarnessX lives only in the execution and improvement planes, behind the extra.
Boundaries. RulesetStore detects, PolicyStore decides severity/scope, ScoreModel optimizes — three owners, three change channels (human PR for patterns/severity; loop-via-EvolveValidator for status/floors/score-constants).
flowchart TB
subgraph GOV["GOVERNANCE PLANE — stdlib, versioned, audited, ZERO-DEP"]
direction LR
POL["PolicyStore<br/>policy/verycode.py<br/>CWE_Score.tsv -> CwePolicy<br/>severity 0-5, static/dynamic"]
RUL["RulesetStore<br/>ruleset/store.py<br/>*.toml: pattern, cwe, status"]
LED["rule_policy.json<br/>loop-owned: status/floor/precision"]
SCO["ScoreModel<br/>scoring/project_score.py<br/>policy-weighted -> 0-100 + gate"]
FPL["FP ledger<br/>false_positive_learnings.json"]
end
subgraph EXE["EXECUTION PLANE — HarnessX runtime, OPTIONAL EXTRA"]
direction LR
DET["Deterministic stages<br/>skip_model MultiHookProcessors"]
HUNT["hunter_pool<br/>HxScanOrchestrator (real LLM loop)"]
VER["VerifyProcessor<br/>llm_judge helpers (+ structural fallback)"]
PSP["PolicyScoringProcessor + ScoreProcessor"]
end
subgraph IMP["IMPROVEMENT PLANE — meta_harness, OPTIONAL EXTRA"]
EVO["improve/evolve.py -> MetaAgent.evolve()<br/>levers: rule | policy"]
BEN["BenchmarkHarness<br/>recall/FP + per-rule attribution"]
end
POL --> DET & PSP & SCO
RUL --> DET & HUNT
LED --> RUL
DET --> HUNT --> VER --> PSP --> SCO
VER --> FPL
EXE -->|trajectories + verdicts| BEN
BEN -->|rule_signals.json + deltas| EVO
EVO -->|gated edits| RUL & POL & LED
SCO -->|gate verdict| CI[("CI gate")]
BEN -->|>=90% recall / <10% FP| CI
New components rationale. PolicyStore/ScoreModel/RulesetStore exist to break the fused regex defect; HxScanOrchestrator/VerifyProcessor exist to host the agent loop and judge; improve/evolve.py+EvolveValidator exist to make adaptation safe (bounded, gated, reversible). Steering compliance: zero-dep default install preserved; detection gate kept as a hard constraint separate from the score reward.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| CLI | cli.py (_run_scan) |
Wires ruleset/policy; Phase 3 routes hunter pool to HxScanOrchestrator |
Unchanged entrypoints when extra absent |
| Governance | Python 3.11 stdlib (tomllib, csv, dataclasses) |
RulesetStore + PolicyStore load | Zero-dep, fail-loud on unmapped CWE |
| Scoring | Python stdlib math |
project_score exponential decay |
Always on, zero-dep |
| Execution runtime | HarnessX @ git+...@<SHA> (optional extra) |
Hunter loop, judge verifier, processors | Lazy import behind _has_harnessx() |
| Self-improvement | HarnessX meta_harness (extra/vendored) |
MetaAgent.evolve + EvolveValidator + journal |
5/6-hook minimal patch |
| Data / Storage | TOML rulesets, TSV policy, JSON ledger/artifacts | Versioned governance + loop overlay | All _target_/JSON-serializable for resume |
| Events | HarnessX lifecycle hooks (on_task_start…on_task_end) |
Stage→hook mapping | HarnessRuntime sync driver retained Phases 0-4 |
| Packaging | pyproject.toml [project.optional-dependencies] |
harnessx extra, pinned SHA |
Core dependencies = [] unchanged |
File Structure Plan
Directory Structure
src/openultrasast/
├── ruleset/
│ ├── store.py # load_ruleset()/write_ruleset(); PatternRule (NO severity); status/min_evidence_level/precision_estimate/version
│ ├── python/*.toml # rules-as-data, one [[rule]] per detector
│ ├── c/*.toml # incl. CWE-120 -> CWE-121 remapped buffer rules
│ └── <lang>/*.toml # same pattern per language
├── policy/
│ ├── verycode.py # load_policy() -> dict[str, CwePolicy]; resolve_severity(); CwePolicy dataclass
│ └── CWE_Score.tsv # vendored verycode data (167 CWEs; header "Flaw Severity " has trailing space)
├── scoring/
│ └── project_score.py # SEV_WEIGHT, REACH_MULT, finding_penalty(), project_score(), gate()
├── harness/ # Phase 3 (extra)
│ ├── build_sast.py # build_sast() factory mirrors examples/coding/build_coding
│ ├── task.py # ScanTask(BaseTask)
│ ├── run.py # HxScanOrchestrator: asyncio.run around model_config.agentic(cfg).run(...)
│ ├── slot_contract.py # SlotContractMixin: reads_slots/writes_slots on MultiHookProcessor
│ └── processors.py # SarifIngest/Preprocess/EntryPoint/Rank/Calibrate/QuickFindings/PolicyScoring/Verify/Score processors
└── improve/ # Phase 4 (extra)
├── evolve.py # orchestrator: benchmark -> delta -> rule_signals.json -> MetaAgent.evolve() -> re-benchmark -> accept/revert
├── rule_validator.py # EvolveValidator._rules / ._policy bound checks
└── rule_autocheck.py # ContractAutoCheckProcessor _check_rule/_check_policy branches
.openultrasast/calibration/
└── rule_policy.json # loop-owned ledger: status/threshold/evidence-floor/precision per rule_id
Modified Files
findings.py— extractPATTERN_RULESto TOML; deletePatternRule.severity;quick_scan_findings(root, targets, rankings, ruleset)takes ruleset arg.calibration.py— re-key fromtags[0](line 262) torule_id; writerule_policy.jsonstatus flips, not only ranking demotion (line 146).benchmark.py— addrule_idtoBenchmarkMiss/BenchmarkFalsePositive; per-rule precision/recall inBenchmarkMetrics; emitrule_policy_delta; replace the inert stub at line 368; addBenchEvaluatorsubclass.verification.py— add LLM-judge path (VerifyProcessoroverllm_judgehelpers); keep structural verifier (reachability_status == "unknown"reject, line 91) as zero-dep fallback.config.py— ruleset/policy/score paths + gate thresholds (K,MIN_SCORE,min_emit_priority,min_emit_precision).cli.py—_run_scanwires ruleset/policy; Phase 3 routes thehunter_poolrun_stagecall (line 122) toHxScanOrchestrator.harness.py— retired in Phase 5 only.pyproject.toml— add[project.optional-dependencies] harnessx = ["harnessx @ git+https://github.com/Darwin-Agent/HarnessX.git@<SHA>"]; coredependencies = []unchanged.harnessx/meta_harness/{journal.py, agent.py, validate_workflow.py, processors/contract_autocheck.py}— vendored/patched seam (Phase 4).
System Flows
Scan pipeline on HarnessX (Phase 3 seam)
The top-level driver stays sync (HarnessRuntime); only the hunter_pool stage opens an async surface via asyncio.run. Deterministic stages run as skip_model=True processors carrying work in typed slots; the SlotContractMixin validates reads_slots/writes_slots on every hook (preserving the ProcessorSpec.reads/writes discipline). If a HarnessX capability is unavailable at runtime, the stage falls back to its existing equivalent and records a degradation in the manifest.
flowchart TD
A["on_task_start: SarifIngestProcessor<br/>-> slot sast.static_hints"] --> B["Preprocess -> sast.targets"]
B --> C["EntryPoint -> sast.entry_points"]
C --> D["on_step_start: Rank -> sast.rankings"]
D --> E["Calibrate (MemoryRetrieval pattern over FP ledger)<br/>demotes sast.rankings"]
E --> F{"HarnessX extra<br/>present & capable?"}
F -->|yes| G["hunter_pool: HxScanOrchestrator<br/>asyncio.run(model_config.agentic(cfg).run(ScanTask))<br/>control/tools/obs processors -> StatefulTrajectory"]
F -->|no / capability missing| G2["fallback run_hunter_pool()<br/>record degradation in manifest"]
G --> H["QuickFindings: regex over load_ruleset()<br/>-> sast.findings (NO severity yet)"]
G2 --> H
H --> I["PolicyScoringProcessor: resolve_severity(policy, rule.cwe)<br/>+ emission decision; drop shadow findings"]
I --> J{"reachability_status<br/>== unknown?"}
J -->|yes| K["VerifyProcessor: REJECTED (structural fallback)<br/>or llm_judge verdict"]
J -->|no| K2["VerifyProcessor: llm_judge build/parse/call<br/>(structural fallback if extra absent)"]
K --> L["record_calibration: lesson/missing -> FalsePositiveLearning<br/>+ per-rule outcome -> rule_policy.json"]
K2 --> L
L --> M["on_task_end: ScoreProcessor -> project_score + gate"]
M --> N["report / sarif / manifest (incl. score block)"]
Self-improvement loop
The benchmark is the regression suite governing the data stores. The objective is maximize project_score subject to recall ≥ 90% AND FP < 10% as a hard feasibility constraint — kept separate so the loop cannot "improve" the number by shadowing rules that hurt real security. Every change is shadow-staged first, novelty/evidence/replay-gated, and reverted byte-for-byte on any failure.
sequenceDiagram
participant B as BenchmarkHarness
participant O as improve/evolve.py
participant M as MetaAgent.evolve()
participant V as EvolveValidator (_rules/_policy)
participant J as journal (compute_attribution/fill_gating)
participant L as rule_policy.json
B->>O: misses(recall) + FPs/verifier-rejects(precision) + per-rule metrics
O->>O: write rule_signals.json [{rule_id, signal, cwe, evidence, suggested_action}]
O->>M: evolve(brief + rule_signals.json)
M->>M: write ruleset/*.toml + policy/overrides.toml + candidates.md + journal Round N
M->>V: validity(canon -> contract -> _rules/_policy -> replay) + policy(novelty + evidence)
alt validation fails
V-->>M: StrictValidationError(kind="rule_change") -> round rejected
else passes
V->>B: re-run benchmark on candidate
B->>J: passed_now vs passed_before
J->>J: compute_attribution(predicted): flipped/still_F/regressed/still_T/absent
alt recall<90% OR FP>=10% OR score regressed OR unpredicted regression
J->>L: fill_gating(reverted) -> restore config byte-for-byte
else
J->>L: fill_gating(accepted) -> persist status/floor/precision
end
end
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1, 1.4 | Hunter pool on real Harness.run(); async confined to one stage |
HxScanOrchestrator |
run.py:HxScanOrchestrator.run() |
Scan pipeline |
| 1.2 | Verify via llm-judge + structural fallback | VerifyProcessor |
processors.py:VerifyProcessor |
Scan pipeline |
| 1.3 | Extra absent → unchanged quick/standard | cli._run_scan, _has_harnessx() |
capability guard | Scan pipeline (F branch) |
| 1.5 | Slot allow-list validated per hook | SlotContractMixin |
slot_contract.py |
Scan pipeline |
| 1.6 | Deterministic stages skip_model |
SAST processors | MultiHookProcessor(skip_model=True) |
Scan pipeline |
| 1.7 | Event/scan_id rename to HarnessX hooks/run_id |
build_sast, journal |
hook map table | Scan pipeline |
| 1.8 | Capability-missing fallback + manifest degradation | HxScanOrchestrator |
manifest degradations |
Scan pipeline (G2) |
| 2.1, 2.4 | Rules-as-data; 3 statuses; no rule severity | RulesetStore | load_ruleset(), PatternRule |
— |
| 2.2, 2.3 | Severity from policy; legacy strings discarded | PolicyStore, PolicyScoringProcessor |
resolve_severity() |
Scan pipeline (I) |
| 2.5 | Shadow excluded from report+score | PolicyScoringProcessor, ScoreModel |
emission filter | Scan pipeline (I,M) |
| 2.6 | CwePolicy 0-5 + static/dynamic; dynamic report-only | PolicyStore | CwePolicy |
— |
| 2.7 | Fail-loud on unmapped enabled-rule CWE | PolicyStore startup invariant | load_policy() check |
— |
| 2.8 | CWE-120 → CWE-121 remap | RulesetStore | TOML data | — |
| 2.9, 5.9, 7.5 | Governance/score/benchmark zero-dep | all GOV components | stdlib-only | — |
| 3.1 | Two levers rule/policy |
improve/evolve.py |
_VALID_LEVERS |
Self-improvement |
| 3.2, 3.3 | No pattern text / no 0-5 severity edits | EvolveValidator |
_rules/_policy |
Self-improvement |
| 3.4 | No enabled→disabled jump (shadow mandatory) | EvolveValidator._rules |
bound check | Self-improvement |
| 3.5 | Parse/status/bounds/dup/CWE-resolve checks | EvolveValidator._rules |
validity phase | Self-improvement |
| 3.6 | Novelty gate on reverted re-proposals | journal.check_novelty |
novelty gate | Self-improvement |
| 3.7 | Replay smoke gate (ruleset boots) | run_synthetic_task_smoke_gate |
replay phase | Self-improvement |
| 3.8, 3.9 | StrictValidationError; journal each edit |
EvolveValidator, journal |
fill_gating |
Self-improvement |
| 4.1, 4.2 | rule_id attribution; per-rule metrics; delta artifact |
BenchmarkHarness | rule_policy_delta |
Self-improvement |
| 4.3 | Delta → rule_signals.json (miss/fp) |
improve/evolve.py |
signal writer | Self-improvement |
| 4.4, 4.5 | Hard accept gate; byte-for-byte revert | improve/evolve.py, journal |
fill_gating(reverted) |
Self-improvement |
| 4.6 | Auto-shadow precision-draggers | improve/evolve.py |
status flip | Self-improvement |
| 4.7 | Coverage-gap nominates rule (human PR) | improve/evolve.py |
rule_signals.json |
Self-improvement |
| 4.8, 6.5 | Score=reward, gate=feasibility (separate) | improve/evolve.py |
objective split | Self-improvement |
| 5.1, 5.2 | 0-100 score; reachability multipliers | ScoreModel | project_score() |
Scan pipeline (M) |
| 5.3 | Dynamic-only/unmapped → 0 penalty, report-only | ScoreModel | finding_penalty() |
Scan pipeline (M) |
| 5.4 | Shadow excluded from score input | ScoreModel | pre-call filter | Scan pipeline (M) |
| 5.5 | Score artifact + manifest block | ScoreModel | score JSON | Scan pipeline (N) |
| 5.6 | FP lowers effective reachability, not delete | calibration.py |
reachability knob | Self-improvement |
| 5.7 | Hard gate: sev5 + reachable → fail | CI Score Gate | gate() |
— |
| 5.8 | Score gate advisory-first, then blocking | CI Score Gate | gate() |
— |
| 6.1, 6.4, 6.6 | Per-merge recall/FP gate + HX tolerance | CI Pipeline | benchmark gate | — |
| 6.2 | Byte-identical baseline Phases 0-2 | BenchmarkHarness | golden diff | Migration |
| 6.3 | Gate must pass on re-benchmarked candidate | improve/evolve.py |
accept gate | Self-improvement |
| 7.1, 7.6 | Empty core deps; pinned extra; non-recursive submodule | Packaging Config | pyproject.toml |
Migration |
| 7.2 | Lazy import behind capability check | _has_harnessx() |
guard | — |
| 7.3, 7.4, 7.7 | Dry-run audit; vendored fallback trigger; pre-specified | Packaging Config | audit gate | Migration (Phase 3 gate) |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
| RulesetStore | Governance | Rules-as-data + status | 2.1,2.4,2.8 | tomllib (P0), rule_policy.json (P1) | State |
| PolicyStore | Governance | CWE→severity/scope authority | 2.2,2.6,2.7 | CWE_Score.tsv (P0) | State |
| ScoreModel | Scoring | 0-100 score + gate | 5.1-5.9 | PolicyStore (P0) | Batch, State |
| HxScanOrchestrator | Execution | Hunter loop on HarnessX | 1.1,1.4,1.8 | HarnessX (P0), RulesetStore (P1) | Service, Event |
| PolicyScoringProcessor | Execution | Resolve severity + emission | 2.2,2.3,2.5 | PolicyStore (P0) | Event |
| VerifyProcessor | Execution | LLM-judge + structural fallback | 1.2 | llm_judge (P1) | Event |
EvolveValidator (_rules/_policy) |
Improvement | Bound-check loop edits | 3.2-3.8 | journal (P0), PolicyStore (P1) | Service |
| improve/evolve.py | Improvement | Benchmark→signal→evolve→gate | 4.1-4.8,6.3 | BenchmarkHarness (P0), MetaAgent (P0) | Batch |
Governance Layer
RulesetStore
| Field | Detail |
|---|---|
| Intent | Load detection rules from versioned TOML; carry loop-controlled status; carry no severity |
| Requirements | 2.1, 2.4, 2.8, 2.9 |
Responsibilities & Constraints
load_ruleset(path) -> tuple[PatternRule, ...]replaces module-levelPATTERN_RULES; loaded withtomllib(zero-dep).PatternRulecarriesrule_id,languages,cwe,tags,pattern,status,min_evidence_level,precision_estimate,version— noseverityfield.status ∈ {enabled, shadow, disabled}is the volume control; the loop overlaysstatus/min_evidence_level/precision_estimatefromrule_policy.json.- C/C++ buffer rules (
c-unsafe-copy,c-unsafe-memory-copy,c-unsafe-scanf) declarecwe = "CWE-121"(remapped from unmapped CWE-120).
Dependencies — Inbound: quick_scan_findings (P0). Outbound: rule_policy.json overlay (P1). External: tomllib (P0).
Contracts: State [x]
Service Interface
def load_ruleset(path: Path, ledger: Path | None = None) -> tuple[PatternRule, ...]: ...
def write_ruleset(path: Path, rules: Iterable[PatternRule]) -> None: ...
@dataclass(frozen=True)
class PatternRule:
rule_id: str
languages: tuple[str, ...]
cwe: str # the ONLY policy join key
tags: tuple[str, ...]
pattern: str # human-PR-only; loop must never edit
status: str = "enabled" # enabled | shadow | disabled (loop-controlled)
min_evidence_level: str = "static_corroboration"
precision_estimate: float = 0.0
version: str = "1"
- Preconditions: every loaded rule has a non-empty
patternand aCWE-NNNcwe. - Postconditions: ledger overlay applied;
status/floors reflectrule_policy.json. - Invariants: no
severityattribute exists;statusis one of the three literals.
PolicyStore
| Field | Detail |
|---|---|
| Intent | Authoritative CWE→severity/scope; resolve finding severity |
| Requirements | 2.2, 2.3, 2.6, 2.7, 2.8 |
Responsibilities & Constraints
load_policy() -> dict[str, CwePolicy]keyed"CWE-NNN"fromCWE_Score.tsv(167 CWEs; severity column header is literally"Flaw Severity "with a trailing space — parse defensively).resolve_severity(policy, rule.cwe, target, ranking)replaces every literalrule.severityread. Policy always wins; any legacy rule severity string is discarded.- Startup invariant (fail-loud): every
enabledrule'scwemust resolve in PolicyStore, else the scan aborts.dynamic-only CWEs are report-only.
Dependencies — Inbound: PolicyScoringProcessor, ScoreModel (P0). External: csv, dataclasses (P0).
Contracts: State [x]
Service Interface
@dataclass(frozen=True)
class CwePolicy:
flaw_category: str
severity: int # 0..5 — AUTHORITATIVE; overrides any rule string; loop must never edit
static: bool # SAST scope gate
dynamic: bool # DAST scope — report out-of-scope, never scored by static tool
def load_policy(tsv: Path = CWE_SCORE_TSV) -> dict[str, CwePolicy]: ...
def resolve_severity(policy: Mapping[str, CwePolicy], cwe: str, target, ranking) -> int: ...
def assert_rules_resolve(rules, policy) -> None: # raises at startup if an enabled rule's CWE is unmapped
...
- Invariants: one CWE ⇒ one severity; two rules for the same CWE always receive identical severity.
Scoring Layer
ScoreModel
| Field | Detail |
|---|---|
| Intent | Compute a 0-100 project score and the two-condition gate verdict |
| Requirements | 5.1-5.9 |
Responsibilities & Constraints
- Pure stdlib (
math), always on, zero-dep — runs in the default install with no HarnessX. - Excludes
shadow-status findings before computing; assigns 0 penalty to dynamic-only/unmapped CWEs (reports them instead). - The
REACH_MULTkeys are exactly the three valuesfindings.pyemits; the reachability multiplier is the FP-calibration knob (a confirmed FP lowers effective reachability, never deletes a rule). - Emits the scan-level artifact and the manifest score block; computes the hard gate (sev5 + reachable) and the score gate (advisory-first).
Contracts: Batch [x] / State [x]
Service Interface
SEV_WEIGHT = {5: 50, 4: 25, 3: 10, 2: 2, 1: 1, 0: 0}
REACH_MULT = {"reachable": 1.0, "inferred-file-surface": 0.6, "unknown": 0.4}
def finding_penalty(f, rule_cwe: str, policy) -> float:
pol = policy.get(rule_cwe)
if pol is None or not pol.static: # dynamic-only / unmapped -> report, don't score
return 0.0
return SEV_WEIGHT[pol.severity] * REACH_MULT.get(f.reachability_status, 0.4)
def project_score(findings, rule_cwe_by_id, policy, K: float = 60.0) -> int:
# shadow-status findings excluded before this call
P = sum(finding_penalty(f, rule_cwe_by_id[f.finding_id], policy) for f in findings)
return round(100 * math.exp(-P / K)) # P=0 -> 100; one critical-reachable -> ~43
def gate(findings, score, *, min_score: int, block_severity_reachable: int = 5,
blocking: bool) -> dict: ...
- Postconditions: artifact JSON (see Data Models) written next to per-finding output and merged into the manifest.
Execution Layer
HxScanOrchestrator
| Field | Detail |
|---|---|
| Intent | Run the hunter pool on a real HarnessX agent loop, confining async to one stage |
| Requirements | 1.1, 1.4, 1.7, 1.8 |
Responsibilities & Constraints
- Invoked from the existing
hunter_poolrun_stagecallback (cli.py:122); wrapsasyncio.run(model_config.agentic(harness_config).run(ScanTask(...))). build_sast()mirrorsexamples/coding/build_coding;ModelConfig(main=hunter, judge=…, ranker=…, patcher=…).agentic(cfg)keeps model info out ofHarnessConfig.- Each hunter sub-harness registers
LoopDetectionProcessor,CostGuardProcessor/TokenBudgetProcessor,ToolFailureGuard,ToolFilterProcessor,SelfVerifyProcessor,EpisodeMetricsProcessor/OTelProcessor/CheckpointProcessor. - Produces a
StatefulTrajectoryper target; rewards backfilled from verifier verdicts. On capability-missing, falls back torun_hunter_pool()and appends to manifestdegradations.
Dependencies — External: HarnessX core + processors (P0, lazy). Inbound: cli._run_scan (P0).
Contracts: Service [x] / Event [x]
Event Contract
- Hook mapping:
stage_start→on_step_start,stage_end→on_step_end,scan_id→session_id/run_id;before/after_model|tooldirect.HARNESSX_CONTRACT_MODE(message-window) +OUS_SLOT_CONTRACT(key-level viaSlotContractMixin) split the old singlecontract_mode.
PolicyScoringProcessor
| Field | Detail |
|---|---|
| Intent | Resolve policy severity per finding and make the emission decision |
| Requirements | 2.2, 2.3, 2.5 |
Responsibilities & Constraints
- Runs
on_after_tool/on_step_end; readssast.rankings+ PolicyStore; writes resolved severity + emit-decision. - Drops
shadowfindings from the reported set and the score input while recording their outcome for precision tracking. - Applies
min_emit_priority/min_emit_precisionthresholds — the new volume controls that replace "every regex match becomes a finding".
Contracts: Event [x]
VerifyProcessor
| Field | Detail |
|---|---|
| Intent | LLM-judge verification with a zero-dep structural fallback |
| Requirements | 1.2 |
Responsibilities & Constraints
- Wraps
build_judge_prompt/parse_judge_response/_call_judge(llm_judge.py) as ajudge-role sub-harness; maps the strict-JSON verdict (plausible|unsupported|hedging|…+lesson/missing) toVerificationResult. - Fallback: the existing structural verifier (
verification.py:91, rejects onreachability_status == "unknown") is used verbatim when the extra is absent or the judge is unavailable.
Contracts: Event [x]
Improvement Layer
EvolveValidator (_rules / _policy)
| Field | Detail |
|---|---|
| Intent | Enforce the bounded-lever safety contract on every loop edit |
| Requirements | 3.2-3.8 |
Responsibilities & Constraints (the gate — runs in the validity phase beside _contract/_replay)
_rules: edit parses;status ∈ {enabled,shadow,disabled};tighten/loosenadjusts only threshold/evidence-floor within configured bounds;adddoesn't duplicate arule_id; everycweresolves in PolicyStore; pattern text unchanged (reject otherwise); noenabled→disabledjump (requireshadow)._policy: touches only evidence floor, scope,K/MIN_SCOREwithin bounds; never the 0-5 severity.- On any failure:
raise StrictValidationError(kind="rule_change")→ the whole round is rejected.
Contracts: Service [x]
Service Interface
_VALID_LEVERS = frozenset({
"configuration", "control", "action", "instruction",
"rule", # status/evidence-floor/threshold in ruleset/ — NEVER pattern text
"policy", # evidence floor + scope + K/MIN_SCORE — NEVER the 0-5 CWE severity
})
class EvolveValidator:
def _rules(self, edit) -> None: ... # raises StrictValidationError(kind="rule_change")
def _policy(self, edit) -> None: ...
improve/evolve.py (orchestrator)
| Field | Detail |
|---|---|
| Intent | Drive benchmark→signal→evolve→re-benchmark→accept/revert under the gate |
| Requirements | 4.1-4.8, 6.3 |
Responsibilities & Constraints
- Converts
rule_policy_delta+ FP learnings →rule_signals.json(recall sidesignal="miss"; precision sidesignal="verifier_reject"|"fp"); callsMetaAgent.evolve(). - Acceptance gate (hard): round accepted only if aggregate recall ≥ 90% AND FP < 10% AND
project_scoredid not regress on the held set AND no unpredicted regression; otherwisefill_gating(outcome="reverted")restores config byte-for-byte. - Precision-dragging rules are auto-shadowed, never deleted; coverage gaps nominate a rule via
rule_signals.json(pattern authoring stays human-PR). - Objective:
maximize project_score s.t. recall/FP gate— score is the reward, the gate is the feasibility region; never folded together.
Contracts: Batch [x]
Data Models
Ruleset policy schema (detection only — TOML)
[[rule]]
rule_id = "python-sql-injection"
languages = ["python"]
cwe = "CWE-89" # the ONLY policy join key
tags = ["injection"]
pattern = '(\.execute)\s*\(\s*[^,)]*%' # human-PR-only
status = "enabled" # enabled | shadow | disabled (loop-controlled)
min_evidence_level = "static_corroboration" # loop may raise within bounds
precision_estimate = 0.62 # backfilled from benchmarks
version = "3"
Status semantics: enabled fires/scores/reports; shadow fires and records outcomes but is excluded from report and score (mandatory staging state for any loop-proposed change); disabled does not fire.
Loop ledger (rule_policy.json) — loop-owned overlay, re-keyed on rule_id:
{ "python-sql-injection": { "status": "shadow", "min_evidence_level": "dynamic_corroboration",
"precision_estimate": 0.41, "outcome": "accepted", "round": 7 } }
Project-score schema (artifact + manifest block)
{ "project_score": 73, "max_severity": 5, "penalty_total": 30.0,
"by_category": {"Command or Argument Injection": 1, "Cryptographic Issues": 2},
"out_of_scope_dynamic_only": 0,
"unmapped_cwe": [],
"gate": {"min_score": 80, "block_severity_reachable": 5, "blocking": false, "passed": false} }
Consistency & Integrity
- Authority direction: governance data is the source of truth; execution reads it, emits findings/signals; only the loop (via
EvolveValidator) writesstatus/floors/score-constants back, never the pattern or the 0-5 severity. - Temporal/versioning:
PatternRule.versionandrule_policy.json.roundgive per-rule audit; the journal records each lever edit;fill_gating(reverted)restores byte-for-byte. - Serialization (resume-safe): all scan slots (
sast.targets,sast.findings,sast.rankings, verifications) use plain dicts/dataclasses with_target_registration so they survivewake().
Error Handling
Error Strategy
| Error type | Condition | Response |
|---|---|---|
| Startup config error | An enabled rule's cwe is unmapped in PolicyStore |
assert_rules_resolve raises; fail loud, abort scan before any work (Req 2.7) |
| Malformed ruleset/policy TOML | tomllib parse failure |
Fail loud at load; CI catches before merge |
| Loop edit violates bounds | Pattern edit / severity edit / enabled→disabled / duplicate rule_id / unmapped CWE |
EvolveValidator raises StrictValidationError(kind="rule_change"); entire round rejected (Req 3.8) |
| Reverted re-proposal w/o rationale | Novelty gate hit | journal.check_novelty blocks; loop continues with other candidates (Req 3.6) |
| Replay smoke gate failure | Edited ruleset/policy won't boot | Round rejected before benchmark re-run (Req 3.7) |
| Acceptance-gate failure | recall<90% OR FP≥10% OR score regressed OR unpredicted regression | fill_gating(reverted) restores config byte-for-byte (Req 4.5) |
| HarnessX capability missing at runtime | Extra absent or sub-harness unavailable | Stage falls back to existing equivalent; record degradation in manifest (Req 1.8) |
| Slot contract violation | Stage reads/writes an undeclared slot | SlotContractMixin raises under OUS_SLOT_CONTRACT=strict; warns under warn (Req 1.5) |
| Slot serialization loss on resume | Non-_target_ slot restores as None |
Per-slot-type round-trip test guards; _target_ registration required |
Monitoring
EpisodeMetricsProcessor.episode_summary→ manifest metrics block;OTelProcessorOTLP export when configured.HarnessJournalwrites per-run{run_id}.jsonl/_trace.jsonl/_state.json, rotated onSegmentBoundaryEvent.- The score artifact
unmapped_cwelist surfaces policy-completeness drift; the journal changeset table surfaces every rule/policy lever edit per round.
Testing Strategy
Unit Tests
load_policy()parses the"Flaw Severity "trailing-space header and yields 167CwePolicyentries with correctstatic/dynamicflags.project_score()boundary cases: P=0 → 100; one severity-5 reachable finding → ~43; one low finding → ~97; shadow findings excluded.resolve_severity()returns policy severity (not the legacy rule string) and is identical for two rules sharing a CWE.EvolveValidator._rulesrejects: pattern edit, severity edit,enabled→disabledjump, duplicaterule_id, unmapped CWE.finding_penalty()returns 0.0 for dynamic-only and unmapped CWEs.
Integration Tests
- Byte-identical baseline (Req 6.2): with the default ruleset and an empty
rule_policy.json,benchmark.pyoutput matches the pre-feature golden corpus exactly. - Startup invariant (Req 2.7): an enabled rule with an unmapped CWE aborts the scan with a loud error.
- Self-improvement round (Req 4.4/4.5): a deliberately noisy rule is auto-shadowed when it drags precision; a round that regresses recall is reverted byte-for-byte.
- Capability fallback (Req 1.8): extra absent →
quick/standardrun through the sync driver unchanged; degradation recorded when a HX stage is requested but unavailable. - Slot round-trip (Req on resume): each scan slot type serializes and restores without loss after
wake().
E2E / Gate Tests
- CI detection gate (Req 6.1): the pipeline fails the merge if recall<90% or FP≥10% on the corpus.
- Two-condition score gate (Req 5.7/5.8): hard gate fails on a sev5+reachable finding; score gate is advisory until
K/MIN_SCOREare calibrated, then blocking. - Zero-dep guard (Req 7.5): with HarnessX absent, governance/scoring/benchmark planes operate and CI gating runs in the default install.
Performance / Tolerance
- HX tolerance (Req 6.4): an enabled HarnessX-backed stage keeps recall/FP within the configured tolerance of the zero-dep baseline.
- Hunter-loop cost/token budgets enforced by
CostGuardProcessor/TokenBudgetProcessor.
Security Considerations
- Supply-chain posture (Req 7): core
dependencies = []is preserved; HarnessX ships only as a SHA-pinned optional extra, lazy-imported behind_has_harnessx(). Apip install --dry-runaudit runs before Phase 3; if the realized transitive graph pulls browser binaries / Docker SDK / a FastAPI web stack as mandatory deps and maintainers judge it unacceptable for a supply-chain auditing tool, the pre-specified vendored lean subset (Option C) is switched in under the same extra name — a packaging change, not a redesign. Submodule init is non-recursive so the largeverlRL fork is never pulled. - Loop containment: the meta-agent can never invent or rewrite detectors (no pattern edits) or rewrite the severity authority (no 0-5 edits);
shadow-first staging, novelty/evidence/replay gates, and byte-for-byte revert bound its blast radius — directly preventing the "too influential" defect from re-entering via the loop. - Gate integrity: the recall/FP gate is a hard constraint that is never folded into the score reward, so the loop cannot "improve" the number by shadowing rules that hurt real security.
Migration Strategy
Every phase is independently shippable, test-gated, and reversible. Phases 0-2 deliver governance value at zero dependency cost and stay byte-identical by default. Phases 3-4 are the HarnessX adoption behind the extra. Phase 5 is the optional native end-state.
flowchart LR
P0["Phase 0<br/>Policy + Score<br/>zero-dep, gate advisory"] --> P1["Phase 1<br/>Rules-as-data<br/>zero-dep, byte-identical"]
P1 --> P2["Phase 2<br/>Benchmark->signal + ledger<br/>zero-dep"]
P2 --> GATE{"pip --dry-run audit<br/>(D2 decision)"}
GATE -->|graph acceptable| P3B["Phase 3<br/>[harnessx] extra (Option B)<br/>hunter loop + verify"]
GATE -->|graph unacceptable| P3C["Phase 3<br/>vendored lean subset (Option C)<br/>same extra name"]
P3B --> P4["Phase 4<br/>Self-improvement loop live"]
P3C --> P4
P4 --> P5["Phase 5 (optional)<br/>full single-step harness + RL<br/>retire HarnessRuntime"]
| Phase | Scope | Dep cost | Merge gate | Reversibility |
|---|---|---|---|---|
| 0 — Policy & score | Vendor CWE_Score.tsv; policy/verycode.py + scoring/project_score.py; CWE-120→121 remap; startup CWE-resolution invariant; emit score artifact + advisory gate. HarnessRuntime/PATTERN_RULES unchanged. |
zero | benchmark byte-identical; score is additive output | gate disable flag |
| 1 — Rules as data | Extract PATTERN_RULES→ruleset/<lang>/*.toml; delete PatternRule.severity; load_ruleset(); resolve_severity; add status + emit thresholds. |
zero | benchmark byte-identical to Phase 0 | keep PATTERN_RULES fallback |
| 2 — Benchmark→signal + ledger | rule_id on misses/FPs; per-rule precision/recall; emit rule_policy_delta; rule_policy.json ledger; calibration writes status flips re-keyed on rule_id. |
zero | empty ledger == current behavior | empty ledger = no-op |
| 3 — HX execution adoption | Run pip --dry-run audit (D2 gate). Add [harnessx] extra (pinned SHA, lazy, zero-dep CI guard); build_sast() + HxScanOrchestrator; hunter pool on real Harness.run(); VerifyProcessor; PolicyScoring/ScoreProcessor. HarnessRuntime stays. |
opt-in heavy | extra-absent: unchanged; extra-present: recall/FP within tolerance | feature flag off |
| 4 — Self-improvement live | Vendor/patch the meta-harness seam; orchestrator benchmark→delta→signal→evolve()→re-benchmark→accept/revert. |
opt-in | ≥90% recall / <10% FP per-round; auto-revert | don't run evolve → ledger-only changes |
| 5 — Full harness + RL (optional) | Migrate deterministic stages to single-step harness + SlotContractMixin; retire HarnessRuntime; promote score gate to blocking; optional rl/. |
opt-in | golden-diff vs Phase 3 empty; gate held | per-stage revert |
Rollback triggers. Any phase that drops recall<90% or raises FP≥10% on the corpus blocks the merge; any self-improvement round failing the acceptance gate reverts byte-for-byte; an unacceptable pip --dry-run graph switches Phase 3 to the vendored subset.