mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
Spec-driven plan to (1) adopt HarnessX (Darwin-Agent/HarnessX) directly where it pays off — the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop — and migrate scan flows onto it stage-by-stage; (2) make detection rules versioned, centrally-governed data whose severity comes from a vendored verycode CWE policy (CWE_Score.tsv) instead of the rule itself, fixing "rules too influential / not centrally managed"; (3) adapt rules/policy inside HarnessX's MetaAgent.evolve loop under bounded levers with replay/novelty/ validator gates; (4) make benchmarks actionable per-rule signals with a hard accept gate and byte-for-byte revert; (5) compute a 0-100 verycode-style project score with a two-condition CI gate. Key decisions captured in research.md: HarnessX is an LLM-agent harness (not a generic stage pipeline), so it is adopted only on the agentic plane behind an optional SHA-pinned extra while the governance/scoring/benchmark planes stay zero-dependency; the >=90% recall / <10% FP detection gate is a hard constraint separate from the score reward and must hold at every phase; CWE-120 is unmapped in verycode so C/C++ buffer rules remap to CWE-121 (severity 5). Artifacts: spec.json (phase tasks-generated, approvals pending), requirements.md (7 requirements, EARS), design.md (three-plane architecture), tasks.md (4 phases), research.md (D1-D9 decisions). Awaiting human approval per the kiro 3-phase gate; not yet implemented. Co-Authored-By: Claude Opus 4.8 <[email protected]>
34 KiB
34 KiB
Research & Design Decisions
Purpose: Capture the discovery findings and architectural investigation behind the harnessx-self-improving-rulesets spec. This is the background-notes companion to design.md — it records what was read, what was found in the source, and why the three resolved decisions came out the way they did. design.md carries the committed architecture; this file carries the evidence and the rejected alternatives.
Summary
- Feature:
harnessx-self-improving-rulesets - Discovery Scope: Complex Integration (adopt an external agent-runtime library + re-architect the in-house rule/policy/scoring planes)
- Key Findings:
- HarnessX and OpenUltraSAST (OUS) already both have a thing called a "harness" with "processors" and "events", but they are incompatible at every load-bearing point: HarnessX is async-generator / message-window-contract / transcript-state; OUS is sync / declared-
reads/writes-contract / dict-state. There is no drop-in. - HarnessX's real, reusable value for OUS is concentrated in three places: the LLM hunter agent loop (control/tools/observability processors), the llm-judge verifier (
build_judge_prompt/parse_judge_response/_call_judge), and the meta-harness self-improvement engine (journal / novelty / evidence / replay / attribution). Everything else (lab web UI, browser tools, sandboxes, office skills) is unused weight. - OUS today fuses detector + severity authority + volume control into one regex layer (
PATTERN_RULESinfindings.py); its self-improvement loop only demotes ranking (floored at0.1, keyed ontags[0]), never the rules; and its benchmark already names the ruleset as the thing to fix (next_improvement_candidate="rules_or_language_hunter") but writes it to an inert JSON. The signals exist; the loop back onto the rules does not. - The
verycodepolicy data (CWE_Score.tsv, 167 CWEs, severity 0-5 + static/dynamic scope) is complete and authoritative, but theverycode-scorescoring algorithm was never implemented (// scorer code hereplaceholder). The score formula in the design is a labeled, defensible reconstruction. - OUS today has zero runtime dependencies (
dependencies = []); HarnessX'sdependencieslist is ~30 heavyweight packages (Playwright, Docker SDK, FastAPI/uvicorn, multi-LLM SDKs, office-doc suite) with no lean extra to pip-install a subset — a direct hard dependency would invert the project's supply-chain posture for a security tool.
- HarnessX and OpenUltraSAST (OUS) already both have a thing called a "harness" with "processors" and "events", but they are incompatible at every load-bearing point: HarnessX is async-generator / message-window-contract / transcript-state; OUS is sync / declared-
Research Log
HarnessX core composition & runtime model
- Context: We need to know whether OUS's
harness.HarnessRuntimecan be replaced by, or interoperate with, HarnessX'sHarness, and how an external project builds and runs a HarnessX harness. - Sources Consulted:
/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,config_schema,config_store,model_config,trajectory}.py(read in full);/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py. - Findings:
- Public composition is three objects + one method:
HarnessBuilder.build() -> HarnessConfig;ModelConfig.agentic(HarnessConfig) -> Harness;await Harness.run(task) -> HarnessResult.HarnessConfigcarries NO model info — model is always supplied separately viaModelConfig(key→provider map,mainrequired, non-mainkeys auto-get sub-harnesses). HarnessBuilderis an immutable fluent factory:add(proc),slot(**kw),add_tool,plugin(source),__or__/merge(collects conflicts →HarnessConflictError),build()(topo-sorts processors per hook by_orderthen Kahn within a bucket by_after). Declarative alt:build_from_config(dict)from a flatprocessors:list of_target_dicts.- The run loop (
run_loop) is a fixed async agent loop:task_start→while True:(step:step_start→before_model→ call provider →after_model→ per-toolbefore_tool/exec/after_tool→step_end) →task_end.get_procs(key)runs"*"(MultiHook) processors on every hook, then hook-specific lists. - State is two message tracks (
raw_messagesappend-only,messageseffective context) + typedslots: dict[str, StateSlot](the nearest analogue to OUS's flatstatedict, but typed and_target_-serializable).StatefulTrajectoryis a first-class product (s_t = (z_t, Δz_t, a_t, o_t, e_t, r_t)), built inline duringrun_loop, withbackfill_rewardsandto_training_records/to_rl_records. - Hook-name alignment vs OUS: 5/8 match exactly (
task_start,before_model,after_model,before_tool,after_tool,task_end); the gap isstage_start/stage_end↔step_start/step_end. Semantically an OUS "stage" is a coarse pipeline phase wrapping a callback (run_stage(name, callback)), whereas a HarnessX "step" is one model-call iteration — not the same concept.
- Public composition is three objects + one method:
- Implications: A whole-pipeline swap is high risk. The cheapest correct adoption confines HarnessX's async loop to the one OUS stage that is an agent loop (the hunter pool) and keeps the sync driver for deterministic stages. This directly motivates Decision D1 (phased seam, not big-bang).
The two incompatible processor contracts
- Context: OUS's defining feature is a declared
reads/writesper-key state contract. We must know if HarnessX preserves it. - Sources Consulted:
/tmp/HarnessX/harnessx/core/{processor,contract_check}.py;/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py:49-118. - Findings:
- HarnessX
Processorisasync def process(event) -> AsyncIterator[Event]— an async generator that yields transformed events. The recommended base isMultiHookProcessorwithon_*hook methods, an@on(EventClass)decorator, and class-attr ordering hints (_order,_after,_singleton_group,_hook). There is nospec, nohandles, noreads/writes. - HarnessX's contract governs message-window mutation, not state keys: enforced in
ProcessorChain.processvia_validate_messages_contract(system stays at index 0 onstep_start;before_modelnet length ∈ {0,+1}; appended message must be roleuser; no out-of-window edits) and per-chaincheck_post_hook_invariants. Mode is the env varHARNESSX_CONTRACT_MODE ∈ {warn(default), strict}— not a constructor arg. - OUS
Processoris synchronous,def handle(event, state: dict) -> dict | None, withspec: ProcessorSpec(name, version, handles, reads, writes);_run_processorenforces state-key discipline (reads must pre-exist; writes must be declared; return must be a dict).ContractModeis a"strict"|"warn"constructor arg.
- HarnessX
- Implications: The two contracts do not map onto each other. OUS's per-key allow-list (its provenance/audit discipline) has no home in HarnessX. To keep that discipline we must reimplement it as a
SlotContractMixinon our HarnessX processors (reads_slots/writes_slotsclass attrs validated in a baseon_*wrapper), gated by our ownOUS_SLOT_CONTRACTenv. This is Decision D7.
HarnessX built-in processors — reuse map
- Context: Which HarnessX processors can OUS register directly, which are reusable as patterns/helpers, and which are irrelevant?
- Sources Consulted:
/tmp/HarnessX/harnessx/processors/(control, tools, multi_model, observability, evaluation, memory, context);/home/mc/Source/OpenUltraSAST/src/openultrasast/cli.py(_run_scan). - Findings:
- Register-direct only inside the hunter loop (the one real agent loop):
LoopDetectionProcessor,CostGuardProcessor/TokenBudgetProcessor,ToolFailureGuard,SelfVerifyProcessor,ToolFilterProcessor/ToolWhitelistProcessor,ModelRouterProcessor,EpisodeMetricsProcessor/OTelProcessor/CheckpointProcessor. - Reuse as helper functions for
verify: thellm_judgecode (build_judge_prompt,parse_judge_responsewith the verdict whitelist, the two-attempt repair loop_call_judge) plus the strict-JSON verdict schema (verdict/confidence/cause/missing_capability/lesson/missing). The judge is explicitly told "ground truth is intentionally withheld" — matching OUS's no-oracle scanning. - Reuse as pattern for
calibrate/record_calibration:MemoryRetrievalProcessor+MemoryPolicy(retrieve/add, should_retrieve/should_store) is the clean interface to refactor the FP ledger toward;MemoryExtractionProcessormirrors distilling verifier outcomes into durable learnings. Storage stays a custom JSON ledger. - Keep custom for every SAST-domain stage:
static_mapping,preprocess,entry_point_mapping,quick_findings,report,sarif,manifest, and the benchmark recall/FP scoring (HarnessXEvalResult.scoreis a single float — too coarse for the recall/FP gate).
- Register-direct only inside the hunter loop (the one real agent loop):
- Implications: Adoption is bimodal — register HarnessX classes directly only in the hunter sub-harness; everywhere else port logic as helpers or patterns. This is the backbone of the §3 flow-migration table in
design.md.
HarnessX self-improvement (meta_harness) and the rule-adaptation seam
- Context: Can an external signal (a verifier rejection or a benchmark miss) drive a detection-rule change through HarnessX's self-improvement loop, and how does it compare to OUS's current calibration?
- Sources Consulted:
/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py,processors/contract_autocheck.py,workers/trajectory_digester.py,rl/{builder,config,task}.py;/home/mc/Source/OpenUltraSAST/src/openultrasast/{calibration,benchmark,findings,rank,cli}.py. - Findings:
- HarnessX self-improves a harness round by round. The unit of change is a
HarnessConfig(the meta-agent literally rewrites the inner agent's config/processors/templates as files); the unit of memory is a markdown journal with per-## Round NYAML frontmatter; the unit of safety is a multi-phase validation gate. - The journal classifies every change by a lever from
_VALID_LEVERS = {configuration, control, action, instruction}and trackspredicted_affectedtask_ids plus agating_attribution(flipped/still_F/regressed/still_T/absent).build_contextrenders a per-lever time-decayed Beta posterior scoreboard (_DECAY=0.9) and an explicit "Reverted hypotheses — do not re-propose" list. EvolveValidator.runenforces validity (canonicalize → contract → diff → replay smoke gate), policy (novelty + evidence, only when the diff is non-empty), advisory.check_noveltyhard-blocks re-proposing a reverted hypothesis or reverted(levers, predicted_affected)signature unless aretry_rationaleis present._evidencerequires acandidates.md## Candidate C-Ncross-referenced by the journal.run_synthetic_task_smoke_gateproves the evolved config boots end-to-end.ContractAutoCheckProcessorgives in-session[AUTO-CHECK]feedback on every authoredprocessors/*.py/tools/*.pyedit.- The concrete seam to drive a rule change: add a lever value to
journal._VALID_LEVERS; carry the external signal in via arule_signals.jsonread-context artifact +_render_task_briefdeliverable; add a validity-phase check (EvolveValidator._rules) that raisesStrictValidationError(kind="rule_change"); add changeset buckets (compute_changeset+journal._CHANGESET_KEYS_ORDER); extendcontract_autocheckto classify rule files. The novelty/evidence/replay/attribution machinery then applies for free. - OUS by comparison: detection rules in
findings.pyare static and never modified. Calibration (calibration.py) only doescalibrated = max(0.1, calibrated - 0.5)on a file's ranking priority (keyed ontags[0], notrule_id) plus a prompt-constraint string — it never disables/tightens/adds a rule, has no novelty gate, no attribution. Benchmark misses produce aBenchmarkCalibrationRecordwhosenext_improvement_candidateis hard-coded to"rules_or_language_hunter"and written to an inertcalibration_records.jsonthat nothing consumes. The recall side is a fully open loop.
- HarnessX self-improves a harness round by round. The unit of change is a
- Implications: OUS has exactly the signals HarnessX needs (verifier rejections = precision side; benchmark misses = recall side) but no loop closing back onto the rules. The meta_harness is the engine that closes it. This motivates Decision D3 (which levers, and their hard bounds) and the §5/§6 design.
The verycode policy & scoring model
- Context: The design needs a central CWE→severity authority and a project score. What does
verycodeactually provide? - Sources Consulted:
/tmp/verycode-policies/{README.md,CWE_Score.tsv};gh repo view norandom/verycode-score(incl.action.yaml,main.go);gh repo view norandom/verycode-lib;/home/mc/Source/OpenUltraSAST/src/openultrasast/findings.py. - Findings:
CWE_Score.tsvis the real artifact: 167 data rows, one per CWE, columnsFlaw Category | CWE ID | CWE Name | Flaw Severity (0-5) | Static (X/empty) | Dynamic (X/empty). Severity distribution: 12×5, 15×4, 97×3, 31×2, 0×1, 12×0. Scope: 154 Static, 45 Dynamic, 13 Dynamic-only, none neither. (Header column 4 is literally"Flaw Severity "with a trailing space.)- The scoring algorithm does not exist.
norandom/verycode-scoreis the unmodifiedthe-gophers/go-action"Tweeter" template with// scorer code hereand a--policy policies/standard.jsonflag;norandom/verycode-libis a one-line stub README. The data shape + the "GitHub Advanced Security findings, weighted by CWE severity" descriptions imply a Veracode-style 0-100 pipeline; the design reconstructs the formula and labels it as such. - Real coverage gap: CWE-120 (Classic Buffer Overflow) is absent from the TSV, but 3 high-value C/C++ rules use it (
c-unsafe-copy,c-unsafe-memory-copy,c-unsafe-scanf). Unmapped → those rules would contribute 0 to the score. CWE-121 (Stack Buffer Overflow) is present at severity 5 and is the precise classic-overflow CWE. - Severity mismatch: OUS rule strings disagree with policy (rules label CWE-78/95/134 "high"; policy rates them 5/critical; rules label CWE-94/502 "high"; policy rates them 3/medium). The policy is the authoritative source.
- The OUS reachability vocabulary is exactly
reachable | inferred-file-surface | unknown(verified infindings.py), which maps cleanly onto aREACH_MULT = {reachable:1.0, inferred-file-surface:0.6, unknown:0.4}exploitability multiplier — and that multiplier is the natural FP-calibration knob (lower effective reachability instead of deleting a rule).
- Implications: Severity must flow from PolicyStore (
CWE_Score.tsv), not from the rule. Day-one fixes: remap the 3 CWE-120 rules to CWE-121 (Decision D5); discard rule string-severities (Decision D4). The score is a stdlib-only reconstruction (Decision D8 ships it advisory-first untilK/MIN_SCOREare calibrated).
Installability & dependency weight
- Context: How does an external project depend on HarnessX, and what does it cost a zero-dependency security tool?
- Sources Consulted:
/tmp/HarnessX/pyproject.toml,harnessx/__init__.py,core/builder.py,plugins/{base,loader,discovery,convert}.py,examples/{coding,research,assistant}/harness.py,extensions/plugins/workflow/plugin.py,.gitmodules;/home/mc/Source/OpenUltraSAST/pyproject.toml. - Findings:
- HarnessX is library-first and effectively not on PyPI (
name="harnessx",version="0.1.0", Beta; consumed from Git).requires-python = ">=3.11"— exact match with OUS, no floor conflict. Depend via a SHA-pinned Git URL (the version string won't move). - Custom stages map cleanly onto HarnessX's plugin model: each SAST stage is a
MultiHookProcessor; group stages + tools + skills into aHarnessPluginmounted viabuilder.plugin(...); load in-process (instance/class) rather than via directory/plugin.json. - Dependency-weight is the headline risk. HarnessX's
dependencies(always-installed, not extras) is ~30 heavyweight packages:anthropic/openai/litellm/tiktoken,fastapi/uvicorn/websockets,playwright(ships browser binaries),docker/e2b,hydra-core/omegaconf/structlog/loguru,opentelemetry-*,mcp,python-pptx/python-docx/openpyxl/reportlab/pdfplumber. There is no lean extra —pip install harnessxpulls everything. OUS currently hasdependencies = []. - Mitigating fact: HarnessX uses pervasive lazy imports (heavy modules imported inside functions). So the heavy deps are installed but only imported on demand — which makes both an optional-extra opt-in and a vendored subset genuinely viable.
- Submodule caveat:
.gitmodulesdeclares a nestedrecipe/verl_harnessX/verl→ a large verl RL fork OUS does not need; never--recursiveinit.
- HarnessX is library-first and effectively not on PyPI (
- Implications: A hard dependency is off the table (it inverts the zero-dep stance and pins to an unmovable Beta). Options narrow to (B) optional extra and (C) vendored lean subset. The lazy-import architecture is what makes B keep heavy code cold by default and makes C a surgical packaging switch. This is Decision D2.
Current OUS rule-system problem analysis
- Context: Establish, in code terms, why the user's complaint ("rules are too influential and not centrally managed") is true.
- Sources Consulted:
/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py. - Findings (six code-level defects):
- Pattern matches dominate output. In quick mode, every non-comment regex match becomes a finding (
quick_scan_findings); the only gate is language. Ranking only sorts; verification rejects only onreachability_status == "unknown". A noisy regex sets output volume and FP rate directly — the rule is the result. - Severity is rule-local.
PatternRule.severityis a string literal copied straight onto the finding (_finding_from_match). Two rules for the same CWE can disagree; nothing reconciles them. - No central store. The ruleset is a Python tuple compiled into the package;
config.pyhas no rule/policy section. You cannot enable/disable/tune a rule without editing source and cutting a release. - Calibration demotes ranking, not rules.
calibrate_rankingonly subtractslearning.demotion(default 0.5) from a file'spriority, floored atMIN_CALIBRATED_PRIORITY = 0.1, keyed ontags[0]. A chronically wrong rule fires at full severity forever; learning targets the wrong object. - No CWE-policy linkage.
cweis free-text used only for rationale text and loose benchmark matching. No table maps CWE → governed severity/enablement/evidence floor. - No project-score weighting. Severity and emission are global constants; the ranking signal (
composite_priority) only re-sorts. A low-severity crypto finding in a fuzzable network parser is never promoted; a high-severity finding in dead code is never demoted.
- Pattern matches dominate output. In quick mode, every non-comment regex match becomes a finding (
- Implications: The regex layer is simultaneously detector, severity authority, and volume knob, while ranking/verification/calibration/benchmark can only act around the rules, never on them. The fix is to split those three roles into three owners (rules detect / policy decides severity & scope / score is the optimization target) with a one-directional authority model (governance drives execution; execution emits signals; the loop edits governance under a gate). This is the organizing principle of
design.md.
Architecture Pattern Evaluation
These are the three "architect angles" that were considered for the overall shape. Each is internally coherent; the synthesis in design.md borrows from all three and resolves their three disagreements (D1/D2/D3).
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Incremental seam | Keep the OUS sync driver; run HarnessX only inside the hunter_pool callback via asyncio.run. Rules→data + policy + score added around the existing pipeline. Dependency = optional extra (B) with vendored (C) as escape hatch. Loop edits a "rule" lever (status/threshold). |
Smallest blast radius; confines the async surface to one stage; keeps the recall/FP gate green per-merge; preserves zero-dep default. | Two harness contracts coexist for a while; HarnessRuntime lingers; deterministic stages don't gain HarnessX trajectories yet. |
Chosen as the base trajectory (Phases 0-4). |
| Native rewrite | Delete HarnessRuntime entirely; the whole scan becomes a single-step HarnessX harness with skip_model=True + slot dataflow; vendored lean subset (C) as the committed default. Loop edits a "rule" lever; patterns stay human-PR'd. |
Cleanest end-state; one runtime, one contract; full trajectory/journal coverage of every stage; smallest auditable dep graph. | Touches every stage at once — too much blast radius to keep the gate green per-merge; vendoring is permanent fork-maintenance burden. | Demoted to optional Phase 5+; its end-state (single-step harness, retire HarnessRuntime) is the long-term target. |
| Policy-first | The whole scan runs as processors on a fixed loop; central CWE policy is the centerpiece; loop edits "rule" + "policy" levers (policy = evidence floors/scope, not the 0-5 CWE severity). Dependency = optional extra (B) default, vendored (C) fallback. |
Strongest governance story; two levers give the loop the right surface to "stop rules being too influential" without inventing detectors or rewriting severity. | Same whole-pipeline-at-once risk as native if taken literally; needs careful bounds so the loop can't corrupt the ruleset. | Contributed the two-lever model (D3) and the bounded-authority safety rules. |
Design Decisions
The three architect angles agreed on the destination (rules→governed data; severity from a central CWE policy; a gated project score; HarnessX hosting the hunter loop and a rule-adapting self-improvement loop). They disagreed on three load-bearing choices. Each is resolved below with the recall/FP gate as the deciding tie-breaker. (D4-D9 are smaller decisions that follow.)
Decision D1: Scope of the HarnessX migration
- Context: How much of the scan moves onto HarnessX, and how fast?
- Alternatives Considered:
- Incremental seam — sync driver stays; HarnessX only in the hunter callback.
- Native rewrite — delete
HarnessRuntime; whole scan becomes a single-step harness immediately. - Policy-first whole-loop — whole scan as processors on a fixed loop.
- Selected Approach: Phased. Adopt HarnessX directly at the hunter loop first (Phase 3); migrate deterministic stages onto a single-step harness only later and optionally (Phase 5+), retiring
HarnessRuntimeonly after the seam is proven and the gate has held. - Rationale: The sync→async + contract mismatch is the single highest-risk item in the whole effort. The native rewrite touches every stage at once — too much blast radius to keep ≥90% recall / <10% FP green per-merge. The incremental seam ships the actual HarnessX value (agent loop, journal,
StatefulTrajectory) at exactly the one stage that is already an agent loop. - Trade-offs: Two harness contracts coexist through Phases 3-4 and
HarnessRuntimelingers; in exchange every phase is independently shippable, test-gated, and reversible. - Follow-up: Golden-output diff gate at each phase; confine
asyncio.runto the hunter callback;SlotContractMixinto preserve key-level discipline.
Decision D2: Dependency strategy
- Context: A zero-dependency security tool considering a ~30-package agent runtime with no lean extra.
- Alternatives Considered:
- (A) Hard dependency in main deps.
- (B) Optional extra
openultrasast[harnessx], SHA-pinned, lazy-imported. - (C) Vendored lean subset under
src/openultrasast/_vendor/harnessx/.
- Selected Approach: (B) is the shipping default; (C) is a pre-committed, triggered fallback gated by an explicit
pip install --dry-runaudit before Phase 3. Coredependencies = []stays; all HarnessX imports are lazy behind a_has_harnessx()check. - Rationale: HarnessX's pervasive lazy imports mean the heavy deps are installed but only imported on demand, so a clean opt-in extra already keeps heavy code paths cold in the default install — with the least maintenance burden, while tracking upstream. (A) permanently inverts the project's stance and pins to an unmovable Beta
0.1.0. (C) adds permanent fork-maintenance and should only be paid for when the installed graph (not the imported graph) is itself unacceptable. - Trade-offs: With B, enabling the extra still inherits the full HarnessX dep set (no lean upstream extra exists); every HarnessX import must be guarded.
- Follow-up: Run the dry-run audit and record the realized transitive graph. Decision rule: if it pulls Playwright browser binaries / Docker SDK / a FastAPI web stack as mandatory transitive deps and maintainers judge that unacceptable, switch to (C) — vendor
harnessx/{core,plugins,processors,bundles,meta_harness,workspace,tools/base}+ thellm_judge/journal/validate_workflow/agent/contract_autocheckmodules, deletelab//browser/sandbox/office subtrees, re-pin a minimal set. The lazy-import design makes the deletion surgical. Never use the recursive submodule path (recipe/verl_harnessX/verl).
Decision D3: What the self-improvement loop is allowed to edit
- Context: The user's core complaint is that rules are "too influential." The loop must be able to govern rules without becoming a new way for them to run wild.
- Alternatives Considered:
- Single
"rule"lever (status/threshold). "rule"lever; patterns stay human-PR'd (native angle)."rule"+"policy"levers, where policy = evidence floors/scope, not CWE severity (policy-first angle).
- Single
- Selected Approach: Two bounded levers.
"rule"=status(enabled/shadow/disabled) + evidence-floor + threshold, never pattern text."policy"= evidence floor + scope + score constantsK/MIN_SCORE, never the authoritative 0-5 CWE severity. Patterns and CWE severities are human-PR-only. - Rationale: This is the precise answer to "stop rules being too influential" — the loop may suppress / stage / raise-the-bar, never invent detectors or rewrite the severity authority. Bounds + mandatory
shadowstaging + auto-revert keep loop authority safe; the meta_harness novelty/evidence/replay/attribution gates apply for free. - Trade-offs: The loop cannot fix a bad pattern itself (it can only
shadowit and nominate a human PR), and cannot re-weight a CWE — both are deliberate guardrails. - Follow-up: Implement
EvolveValidator._rules/._policyraisingStrictValidationError(kind="rule_change"); enforce the four "may NOT" bounds (no pattern edits, no 0-5 severity edits, noenabled→disabledwithoutshadow, no reverted re-proposal withoutretry_rationale).
Decision D4: Severity authority
- Context: Two rules for the same CWE can disagree on severity today.
- Selected Approach: PolicyStore (
CWE_Score.tsv) is authoritative;resolve_severity(policy, rule.cwe, target, ranking)replaces the literalrule.severityread;PatternRule.severityis removed. Policy always wins; rule string-severities are discarded. - Rationale: One CWE = one governed severity, reconciled centrally; ends rule disagreement.
- Trade-offs: Loses the per-rule severity hint (acceptable — it was an authoring accident, not a governed decision).
Decision D5: The CWE-120 coverage gap
- Context: 3 C/C++ buffer rules use CWE-120, which is absent from
CWE_Score.tsv. - Selected Approach: Remap
c-unsafe-copy/c-unsafe-memory-copy/c-unsafe-scanfto CWE-121 (Stack Buffer Overflow, severity 5) in the ruleset TOML. - Rationale: CWE-121 is the precise classic-overflow CWE and is already at severity 5; this avoids forking the upstream policy file. Otherwise those high-value rules contribute 0 to the score.
- Follow-up: Add a hard CI invariant — every
enabledrule'scwemust resolve in PolicyStore (fail loud at startup); logunmapped_cwein the score artifact; PR genuine gaps upstream to verycode-policies.
Decision D6: Response to a confirmed false positive
- Context: How does the system react to an FP without hurting recall?
- Selected Approach: Lower the finding's effective reachability multiplier and/or
shadowthe rule — never delete it. - Rationale: The score stops over-penalizing while the rule keeps firing (tracked, just not scored/reported), preserving ≥90% recall and pushing FP <10%. This is exactly the recall/FP balance the project gates on.
Decision D7: Where OUS's reads/writes contract lives
- Context: HarnessX has no per-key state contract; OUS's defining discipline must survive.
- Selected Approach: Reimplement as a
SlotContractMixinon ourMultiHookProcessors (reads_slots/writes_slotsclass attrs validated in a baseon_*wrapper), gated byOUS_SLOT_CONTRACT=strict|warn. - Rationale: The two contracts are incompatible; the mixin preserves provenance/audit discipline inside HarnessX's runtime without trying to bend the message-window contract.
Decision D8: Project-score gate rollout
- Context: The verycode score formula is a reconstruction (no upstream implementation).
- Selected Approach: Ship the score gate advisory-first (computed and reported, not blocking); promote to blocking only after
K/MIN_SCOREare calibrated against the benchmark corpus. Score computation is stdlib-only and always on (zero-dep), so CI gating runs in the default install with no HarnessX present. - Rationale: The formula needs corpus calibration before it can safely block a merge; keeping it zero-dep keeps the CI gate independent of the optional harness.
Decision D9: RL adoption
- Context: HarnessX has an
rl/subsystem (weight-level improvement) alongside the config-level meta_harness. - Selected Approach: Optional, Phase 5+.
- Rationale: Config-level adaptation via the meta_harness is sufficient to close the rule/policy loop; weight-level RL over hunter trajectories is purely additive and not on the critical path.
Risks & Mitigations
- Sync↔async + contract incompatibility (High) — confine the async surface to the hunter stage via
asyncio.runin its callback (Phase 3);SlotContractMixinfor key-level discipline; deterministic stages runskip_model; golden-output diff gate at each phase. - Dependency posture inversion (High) — governance/scoring planes stay zero-dep; HarnessX strictly behind the extra + lazy imports + pinned SHA; CI zero-dep guard test;
pip --dry-runaudit before Phase 3; vendored-subset fallback pre-specified. - Benchmark-gate regression from rule auto-tuning (High) — the recall/FP gate is the per-round acceptance test (aggregate + per-rule, hard constraint);
shadowbeforedisabled; novelty + attribution block thrash and reverted re-proposals; auto-revert byte-for-byte. - Score gaming the gate (High) — keep the gate as a hard constraint, never part of the reward; track unpredicted regressions; add a score-regression check to acceptance.
- Meta-agent corrupts the ruleset, re-introducing "too influential" (Medium) — bounded levers (D3): no pattern rewrites, no severity edits, mandatory
shadowstaging, noenabled→disabledjump; recall gate auto-reverts; every change journaled and novelty-checked. - Policy completeness / CWE drift (Medium) — day-one CWE-120→CWE-121 remap; hard CI invariant that every enabled rule's CWE resolves;
unmapped_cwelogged; treat verycode-policies as authoritative-upstream and PR gaps there. - Frozen Beta upstream (Medium) — pin SHA; document in
VENDOR.md; keep the meta_harness patch minimal/isolated; rule lever degrades gracefully (absent patch = no rule lever, loop still does ranking demotion); vendored subset removes upstream-instability exposure for depended modules. - Slot serialization loss on resume (Medium) — all scan slots use plain dicts/dataclasses with
_target_registration; per-slot-type round-trip serialization test (non-serializable content restores asNoneonwake()). - Two-store coordination drift (Low-Med) — a single
resolve_severityowns the rule-status + policy-gate combination; the changeset records both lever edits per round; the benchmark regression suite catches interaction regressions.
References
- HarnessX core —
/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,model_config,trajectory}.py— composition/run-loop/contract/state model. - HarnessX processors —
/tmp/HarnessX/harnessx/processors/{evaluation/llm_judge.py, evaluation/strategies/evaluators/prm.py, observability/{otel_proc,episode_metrics,checkpoint}.py, memory/memory_retrieval.py, control/loop_detection.py}— reuse map (verify/calibrate/observability). - HarnessX meta_harness —
/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py,processors/contract_autocheck.py,workers/trajectory_digester.py;rl/{builder,config,task}.py— self-improvement engine + rule-adaptation seam. - HarnessX packaging —
/tmp/HarnessX/pyproject.toml,harnessx/__init__.py,plugins/{base,loader,discovery,convert}.py,examples/{coding,research,assistant}/harness.py,extensions/plugins/workflow/plugin.py,.gitmodules— installability + dependency weight. - verycode policy —
/tmp/verycode-policies/{CWE_Score.tsv,README.md};gh repo view norandom/verycode-score(action.yaml,main.goplaceholder);gh repo view norandom/verycode-lib(stub) — policy data (complete) vs scoring algorithm (never implemented). - OUS current system —
/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py—PATTERN_RULES, ranking-only calibration (MIN_CALIBRATED_PRIORITY=0.1), inertnext_improvement_candidate, syncHarnessRuntime, no rule/policy config section. - Companion design —
.kiro/specs/harnessx-self-improving-rulesets/design.md(committed architecture); detection gate perMEMORY.md: ≥90% recall / <10% FP on the benchmark corpus.