Skip to content

cve-agent: native OpenAI-compatible backend + corrector/agent reliability fixes - #105

Merged
danielturull merged 68 commits into
mainfrom
feature/openai+fixes
Sep 7, 2026
Merged

danielturull merged 68 commits into
mainfrom
feature/openai+fixes

Conversation

@danielturull

Copy link
Copy Markdown
Contributor

Summary

Adds a native OpenAI-compatible backend for cve-agent (typed tool loop, bounded Chat Completions client, Git/filesystem/build tools, named provider profiles, Ollama support) and a large set of related cve-corrector/cve-agent reliability fixes and test coverage built on top of it:

  • Native openai backend: bounded chat client, typed Git/filesystem/build/commit tools, agent tool loop, named profile parsing (etc/openai-<profile>.cfg), provider capability dialects/probe/fallback, Ollama provisioning
  • Corrector-to-agent handoff hardening: repository handoff validation, cross-layout patch transfer verification, dependent-fix-chain handling, submodule cleanup fixes
  • Result/audit model: versioned workflow vs. security results, durable per-attempt artifacts, semantic security validation gate
  • Evaluation tooling: reproducible backend evaluation harness, isolated LLM backport capability suite, adversarial release gate, benchmark roster/report updates
  • Numerous integration test expectation fixes and new unit/integration test coverage across corrector, agent, and benchmark tooling

Size note

This branch is 67 commits / ~39k lines across 143 files (git diff --stat origin/main..feature/openai+fixes). The work is highly sequential — later commits (fixes, tests) depend on backend/infra introduced earlier in the same branch — so it is submitted as a single PR rather than split arbitrarily, which would risk non-buildable intermediate states. Happy to split into smaller reviewable PRs along natural seams (e.g. native OpenAI backend core vs. evaluation/benchmark tooling vs. corrector reliability fixes) if maintainers prefer — let me know which boundaries you'd like.

Testing

Existing test suite covers the new code (tests/agent, tests/corrector, tests/benchmark, tests/integration); see individual commits for per-change test additions.

- validate typed file operations against exact authorized roots
- bound inspection, search, mutation, and model-visible results
- share a session deadline across filesystem operations
- cover path traversal, symlink, race, size, and policy failures

Assisted-by: codex:gpt-5
- expose fixed inspection and cherry-pick operations without arbitrary argv
- constrain revisions, pathspecs, environment, output, and deadlines
- validate staged and changed paths against the authorized source scope
- cover repository state, conflict, rollback, and hostile input paths

Assisted-by: codex:gpt-5
- compose file and Git runtimes behind one closed host contract
- run devtool builds with bounded logs, process groups, and deadlines
- require interactive approval for side effects when configured
- create host-owned terminal conclusions only after state validation

Assisted-by: codex:gpt-5
- resolve CLI, environment, and portable endpoint defaults explicitly
- require a model and validate API-key indirection without exposing secrets
- gate remote endpoints and insecure transport behind separate opt-ins
- keep endpoint availability checks local and network-free

Assisted-by: codex:gpt-5
- send portable non-streaming requests through the existing requests dependency
- validate response headers, sizes, JSON shape, messages, and tool calls
- bound retries by one shared deadline and classify transport failures
- redact bearer and configured credentials from diagnostics

Assisted-by: codex:gpt-5
- preserve validated assistant and tool messages across bounded turns
- dispatch only advertised typed tools and validate argument trees
- retain a protected bounded JSONL transcript for every session
- terminate on protocol, deadline, budget, and no-progress failures

Assisted-by: codex:gpt-5
- register the backend and expose validated CLI configuration
- assemble backend-specific tool instructions without changing Kiro or Claude
- connect the client, loop, typed runtime, transcript, and guarded session
- cover configuration, end-to-end runtime, compatibility, and live opt-in paths

Assisted-by: codex:gpt-5
- run hook removal, scope reversion, and audit generation on errors
- retain the primary exception after all cleanup attempts
- surface cleanup failures instead of silently skipping guards

Assisted-by: codex:gpt-5
- Exercise the native backend through a loopback HTTP server and real
  Git
- Cover conflict resolution, protocol failures, cleanup, and redaction
- Separate session tool budgets from single-response client bounds
- Isolate the optional live Ollama smoke test in a disposable workspace

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- Add copy-pasteable local and remote endpoint configuration
- Describe portable API, security, approval, and transcript contracts
- Reconcile maintainer summaries and backend plugin guidance
- Test documented options, precedence, examples, and response shape

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- Add constrained follow-up commit and amend operations
- Preserve build validity when recording already-built source changes
- Reject unsafe index state, conflicts, messages, and Git metadata
- Cover repair workflows, failures, approval, and adversarial inputs

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- reserve and validate openai-<profile> backend selectors
- securely read strict bounded profile files
- validate portable chat and Ollama profile settings

Assisted-by: codex:gpt-5
- inspect and prepare dedicated model aliases through fixed APIs
- verify context and tool capabilities under one session deadline
- require approval for model creation and recreation

Assisted-by: codex:gpt-5
- resolve profile selectors through CLI and context generation
- apply allowlisted chat settings to portable requests
- prepare configured Ollama aliases before model sessions

Assisted-by: codex:gpt-5
- add a strict schema-v2 outcome and failure taxonomy
- migrate legacy JSON and integration CSV results without inventing
verification
- expose unverified workflow completion in logs, summaries, and resume
gates

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- create restrictive result directories and transcripts before preflight
- mirror bounded provider, tool, mutation, build, and lifecycle events
- centralize redaction, telemetry, secret scanning, and artifact hashes

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- capture complete NUL-safe repository state before backend handoff
- classify initialization failures with stable actionable codes
- reproduce and resolve large dirty-status initialization failures

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- bind agent scope to a versioned manifest with repository fingerprints
- require explicit merge mainlines and narrowly restore generated files
- reject drift and out-of-scope dirt before provider invocation

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- map source paths with exact, configured, or strong content anchors
- apply bounded text adaptations with rollback and exact path postchecks
- retain transfer manifests and propagate mapped paths into agent scope

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- validate commit, amend, and cherry-pick parent/tree transitions before
advancing trusted state
- keep build freshness tied to content mutations rather than staging or
commit IDs
- bind later-session baselines to validated handoff provenance and
durable audit artifacts

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- add SHA-256-guarded unique-context edits without raising the full-file
write limit
- enforce strict path, text, size, line, race, atomicity, and
postcondition checks
- cover interactive approval and the deterministic HTTP-to-finish
workflow

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
Add trusted offline reference manifests and conservative semantic
validation for completed agent repairs. Persist bounded review
artifacts, gate release acceptance on verified or equivalent outcomes,
and cover exact fixes, adaptations, prerequisites, omitted tests, path
mappings, merge parents, and false-positive patterns.

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
Replace call-tuple progress heuristics with canonical host evidence
digests, bounded state summaries, staged no-progress handling, and
classified budget failures. Preserve terminal validation budget after
late mutations and add hashed targeted context with bounded build
diagnostics and explicit security prerequisites.

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- describe an allowlisted portable provider dialect
- retain bounded credential-free failure evidence
- version and digest the capability contract

Assisted-by: codex:gpt-5
- encode only profile-declared request fields
- classify bounded HTTP and protocol failure evidence
- preserve reasoning replay and shared loop counters

Assisted-by: codex:gpt-5
- exercise fixed chat, tool-call, continuation, and final turns
- avoid repository data and arbitrary provider-specific fields
- report stable capability error codes

Assisted-by: codex:gpt-5
- validate capability and conformance-probe sections
- constrain fallback selectors to one distinct named profile
- reject nested and unsupported fallback policy values

Assisted-by: codex:gpt-5
- preserve one deadline, transcript, tool runtime, and loop budget
- validate trusted Git state before a declared fallback
- limit fallback to classified provider failures and profile policy

Assisted-by: codex:gpt-5
Run baseline health, full backend, crossover, fallback-policy, and
stability campaigns from immutable manifests and independent clean
snapshots. Keep legacy resume imports explicitly unverified and prevent
rows from being copied across backend cohorts.

Aggregate decomposed trusted telemetry into deterministic security-first
JSON, CSV, and Markdown reports with strict comparison guards and
separate baseline denominators.

Assisted-by: codex:gpt-5
Signed-off-by: Anders Heimer <anders.heimer@est.tech>
- document deterministic hostile-input and provider checks
- connect evaluation evidence to release acceptance
- align component and workflow summaries with the gate

Assisted-by: codex:gpt-5
test_merge_paths_require_and_verify_explicit_mainline built a merge by
checking out "master", but _repo runs a plain "git init" -- so the branch
is whatever init.defaultBranch says. On a host configured with "main" the
checkout failed in setup and the test errored before reaching any
assertion about mainline selection, which is what it exists to cover.

Ask git for the name instead, as the sibling merge test in this file
already does. Verified passing with init.defaultBranch set to both main
and master.

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Detect a rewritten commit subject directly, not only via a recognized
  Conflicts Resolved: marker, so an AI-fabricated commit with no marker
  at all is still caught
- Reset --author and GIT_AUTHOR_DATE to the upstream commit's identity
  when restoring the message, since git commit --amend never changes
  authorship on its own
- Without this, an AI backend that creates a brand-new commit instead of
  amending the cherry-picked one keeps its own author/date/subject in
  the exported patch's From:/Date: headers even after message-text
  restoration

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Issue one 'git checkout devtool -- <files...>' call per chunk of up to
  200 files instead of one subprocess per missing file, and the same for
  the unstaging 'git reset HEAD' step
- Trees with hundreds of missing generated files (autotools output,
  secondary-tarball payloads) previously paid one process spawn per
  file; this collapses that to a handful of calls
- Chunked at 200 files per call to stay under typical platform ARG_MAX
  limits for very large file sets

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- _verify_and_commit() recreates the transferred change as a brand-new
  commit via 'git commit -F' on the target branch, which (unlike
  cherry-pick or am) always authors as the ambient git identity — every
  transfer was silently discarding the original contributor's name,
  email, and date regardless of what came before it in the pipeline
- Set GIT_AUTHOR_NAME/EMAIL/DATE from the source commit before creating
  the transferred commit, so the exported patch's From:/Date: headers
  match the upstream author instead of whoever ran the corrector
- Committer identity is left untouched — only authorship of the
  transferred change is restored

This is the actual root cause behind the CVE-2025-6021/libxml2 case: the
CVE-branch commit's author was already correct by the time
cherry_pick_to_devtool() ran, but transfer_commits() then recreated the
commit on the devtool branch — the branch devtool finish exports the
patch from — discarding it every time.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add AIBackend.verify() -> VerifyResult, defaulting to an is_available()
  presence check so extra/ plugin backends keep working unmodified
- Implement real checks for kiro/claude: a bare CLI invocation (no agent
  config, tool allow-lists, or workspace) asking for a fixed marker
  string back, run in a hermetic temp directory
- Implement openai backend's check by reusing the existing
  ProviderConformanceProbe instead of a new minimal probe, since that
  backend runs tools in-process and tool-call conformance is its
  meaningful health check
- Wire --verify-backend into the existing required --cve-id/--cve-list
  mutually exclusive group so argparse enforces exactly one of the three
  with no new validation code; prints a pass/fail line and exits
  0/EXIT_AGENT_ERROR without touching the CVE workflow
- Document the flag in README.md and the AIBackend.verify() plugin hook
  in extra/README.md

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Resolve and seed GIT_AUTHOR_*/GIT_COMMITTER_* from the operator's
  global git config at cve-agent startup (shared.git_runner.
  resolve_git_identity, cve_agent.__main__._seed_git_identity_env),
  fixing 'Committer identity unknown' failures in sandboxed openai-
  backend sessions that otherwise have no usable Git identity.
  Explicit env vars set by the caller are never overwritten, and the
  sandboxed executor still never reads global/system git config
  directly.
- Add a revert_to_baseline typed tool (openai_git_tools.py) that lets
  the model discard its own typed file changes back to the session
  baseline before escalating via finish(needs_human/not_applicable),
  reusing the existing _discard_typed_mutations rollback internals
  instead of duplicating them.
- Document both in AGENT_INSTRUCTIONS.md, docs/openai-compatible-
  backend.md, and README.md.
- Add an autouse fixture in tests/conftest.py to snapshot/restore Git
  identity env vars around every test, preventing the new startup
  seeding from leaking a real global identity into unrelated tests.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add cve_agent/openai_console.py with a pure, total
  format_console_line() dispatching a small subset of transcript
  events (tool_request, tool_result, terminal_result, session_end,
  session_error, progress_warning, retry, http_failure) to terse
  [#sequence]-prefixed lines
- Add an optional console writer to JSONLTranscript, delegated to
  from write() after the mandatory audit write succeeds and wrapped
  in contextlib.suppress(OSError) so console output can never affect
  the audit path; default None preserves existing behavior
- Wire console=None if interactive else <stdout writer> into both
  the primary and fallback transcript_factory() call sites in
  OpenAICompatibleBackend, so non-interactive sessions stream live
  tool-call progress while interactive sessions stay silent on this
  path, with no new flag
- Document the new streaming behavior and list the new module in
  AGENTS.md's directory map
- Add unit and end-to-end tests covering line formatting, transcript
  byte-identity with/without a console writer, OSError suppression,
  and live stdout streaming for non-interactive/interactive/fallback
  sessions

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add an assistant_response formatter to openai_console.py that
  prints the model's visible commentary text ("model: <text>") for
  turns where it produced any alongside its tool calls; tool-call-only
  turns (empty or missing content) are skipped without printing an
  empty line
- The full, untruncated text is printed, matching what is already
  recorded in the JSONL transcript's assistant_response.content field
- The provider's hidden/opaque reasoning-replay field remains
  unlogged and unstreamed; only the model's already-visible content
  is surfaced
- Update docs/openai-compatible-backend.md to describe the expanded
  streamed subset
- Extend unit and end-to-end tests to cover the new formatter,
  including the tool-call-only skip case and silence during
  interactive sessions despite commentary being present

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Upstream commit 3fb6b31c7166 itself has a defect: it strips ".." from
every link_target, including symlink targets, which legitimately may
start with "../" or "/". The only correct fix for the resulting
ptest regression requires gating the strip to hardlinks only
(typeflag '1') -- a real, necessary deviation from the literal
upstream diff.

Semantic validation correctly cannot auto-verify a deviated patch
against upstream, so the release gate reports SECURITY_REVIEW_REQUIRED
and cve-agent exits 14 (EXIT_AGENT_ERROR) rather than 0, even though
the backport is correct and fully built/tested. Update the test's
expected exit code and the README test table to match this by-design
outcome instead of treating it as a failure.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
cherry_pick_to_devtool() cleans and checks out the superproject before
calling transfer_commits(), but git clean/checkout never touch a
submodule's own working tree. Recipes whose upstream vendors
dependencies as submodules (e.g. coreutils' gnulib) can leave the
submodule checked out at the wrong commit or with leftover untracked
content after switching to devtool, so transfer_commits()'s strict
clean-tree check aborts with TRANSFER_APPLY_FAILED: target tree is
not clean -- an unrecoverable exit that never gives the agent a
chance to retry.

Add reset_submodules() (git_ops.py), a no-op unless .gitmodules
registers submodules, that re-inits and force-resets them to match
the superproject. Call it in cherry_pick_to_devtool() right after
checking out devtool.

Add a regression test with a real git submodule fixture reproducing
the coreutils/CVE-2024-0684 scenario; verified it fails without the
fix and passes with it.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
…lure

transfer_commits() computed 'git status --porcelain=v2 -z' to check the
target tree is clean before planning a transfer, but discarded the
output entirely -- a failure surfaced only as the bare string 'target
tree is not clean', with no path to start debugging from. This made
diagnosing dirty-submodule cases (see the reset_submodules fix) purely
guesswork from the corrector's log alone, since git_clean_workspace/
git checkout calls are never logged.

Add _summarize_dirty_status() to parse the porcelain v2 record shapes
(ordinary changes, untracked/ignored, and submodule S... entries) and
include the offending paths (bounded by the existing
MAX_TRANSFER_DIAGNOSTIC_PATHS) in the TransferError message.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The only conflicting line when cherry-picking the openssh fix is
OpenBSD's "$OpenBSD: clientloop.c,v ..." CVS version-stamp tag --
present on essentially every upstream openssh commit and always
diverging from the stable branch's own stamp. No conflict resolution
can reproduce the literal upstream diff exactly here: keeping
upstream's stamp value differs from what the branch's own pre-image
expected, and keeping the branch's value differs from what upstream's
diff explicitly sets.

Semantic validation correctly flags this as a real (if functionally
inert) byte-level deviation and reports SECURITY_REVIEW_REQUIRED, so
cve-agent exits 14 rather than 0 even though the backport is correct
and builds cleanly (devtool finish succeeds, patch exported). Update
the test's expected exit code and the README test table to match this
by-design outcome, same as the CVE-2026-26157 case.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The corrector applies this monorepo commit cleanly with zero conflicts
and zero AI involvement, but devtool finish deletes the workspace
directory as its last step on EXIT_SUCCESS. By the time the agent's
get_workspace_path() checks for it, there is nothing left to run
semantic validation against.

Per docs/result-schema.md this is by design: build success alone is
never proof the security fix is preserved, so the outcome is
security_status=not_evaluated (WORKFLOW_COMPLETED_UNVERIFIED) rather
than verified/equivalent, and cve-agent exits 14 (EXIT_AGENT_ERROR)
under the default --security-gate=equivalent even though the backport
is a byte-for-byte clean upstream cherry-pick with no deviation at
all. Update the test's expected exit code and the README test table
to match, same pattern as the CVE-2026-26157 and CVE-2024-39894 cases.

Note: unlike those two cases, nothing here required a real deviation
from upstream -- the unverified status is purely an artifact of
workspace-cleanup timing. Whether get_workspace_path's already-
finalized fallback should preserve enough evidence for a genuinely
clean, zero-deviation corrector-only apply to reach 'equivalent'
automatically is a separate design question, out of scope for this
test-expectation fix.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The header scenario list (tests 6, 11, 13) still described the old,
now-incorrect exit-0 expectations after the inline comments and
run_test calls were updated to exit 14 for the CVE-2026-26157,
CVE-2024-39894, and CVE-2024-44331 agent cases. Add a short pointer
from each header entry to its inline comment/rationale so the two
stay consistent.

Also flag test 7 (CVE-2024-0684 / coreutils) in the header: its
expected exit is still under investigation (TRANSFER_APPLY_FAILED /
exit 5 from a dirty gnulib submodule), so the header no longer implies
0 is confirmed correct for it.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The previous reset_submodules() fix (81a4fe8) gated all cleanup behind
'.gitmodules exists in the current working tree'. That check reads
.gitmodules from whatever branch is currently checked out -- but by
the time reset_submodules() runs, cherry_pick_to_devtool() has already
switched to devtool. Real-world coreutils/CVE-2024-0684 confirmed the
actual failure mode: devtool's branch (built from a release tarball)
has no .gitmodules and no gnulib gitlink at all, while the CVE
branch's upstream history does vendor gnulib as a real submodule. The
old check saw no .gitmodules on devtool and no-opped, leaving the
already-initialized gnulib/ directory on disk. git status then reports
it as a plain untracked directory (?? gnulib/), not a submodule
status, and ordinary 'git clean -fdx' silently leaves it because git
refuses to descend into a directory containing its own .git unless
forced twice -- so transfer_commits' clean-tree precheck still failed
with the exact message seen live: 'target tree is not clean: gnulib/'.

Fix: keep resetting submodules that *are* registered on the current
branch (the original wrong-commit case), and unconditionally run
'git clean -ffdx' (double force) afterwards to remove any leftover
nested-repository directories the ordinary single-force clean cannot
touch, regardless of whether the current branch declares them.

Add test_transfer_survives_submodule_untracked_on_devtool, reproducing
this exact scenario (devtool branch with no gnulib path at all).
Confirmed it fails with the old implementation with the identical
'target tree is not clean: gnulib/' message, and passes with the fix.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
With the gnulib submodule fix (31e5b88) in place, a live rerun confirms
cve-corrector now completes CVE-2024-0684 cleanly end to end: the
upstream commit cherry-picks and transfers with zero conflicts
('Transfer verified: 1 mapped entries, 1 final paths'), the build and
before/after ptest both pass (504 passed, 0 failed), and devtool
finish exports the patch successfully (cve-corrector exit 0).

Because the corrector needed no AI help at all, devtool finish removes
the workspace before the agent's get_workspace_path() can find it to
run semantic validation, so the outcome is
security_status=not_evaluated (WORKFLOW_COMPLETED_UNVERIFIED) rather
than verified/equivalent, and cve-agent exits 14 (EXIT_AGENT_ERROR)
under the default --security-gate=equivalent. This is the identical
by-design pattern already fixed for the CVE-2024-44331 case (test 13).
Update the test's expected exit code, its header comment, and the
README table to match, and drop the 'under investigation' note now
that the underlying submodule bug is confirmed fixed.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
…test

Test 7 targets the gnulib submodule dirty-tree fix
(cve_corrector/git_ops.py's reset_submodules), which lives entirely
in cherry_pick_to_devtool/transfer_commits -- a step that runs
unconditionally regardless of --skip-build/--skip-ptest. Those flags
only gate devtool build and ptest execution, and cve-agent has no
--skip-build flag at all, so the previous agent-runner form of this
test paid for a full build+ptest cycle (or, before that, a full
build+ptest cycle that never even completed the corrector's success
path) without exercising anything extra relevant to the fix under
test. It also meant the test's real outcome was cve-agent's post-hoc
'workspace already finalized' exit 14, one hop removed from the
actual regression being verified.

Switch to invoking cve-corrector directly with --skip-build
--skip-ptest, matching the pattern already used by tests 1, 9, and 12
for corrector-only regression cases. This exercises the identical
cherry_pick_to_devtool/transfer_commits path in a few seconds with no
bitbake invocation at all, and asserts cve-corrector's own exit code
(0) directly instead of cve-agent's semantic-validation exit code.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
When cve-corrector applies a CVE fix with no conflict, it runs
devtool finish itself before cve-agent gets a chance to intervene,
removing the source workspace (and its git history) before
_run_cve_pipeline's get_workspace_path() check ever runs. That branch
returned SUCCESS with no outcome argument, so CveResult.__post_init__
fell back to the legacy-status shim migrate_legacy_status("success",
build_evidence=True), which reports SecurityStatus.NOT_EVALUATED with
no failure_class/failure_code at all.

This is indistinguishable, to any downstream reader of the durable
result.json (in particular tests/benchmark/run_benchmark.sh's
benchmark_artifact_outcome and cve_agent/__main__.py's
security_gate_satisfied), from a run that never attempted semantic
validation for an unknown reason. Since the default --security-gate
is 'equivalent', every one of these clean, zero-conflict,
zero-AI-involvement successes exits 14 (EXIT_AGENT_ERROR) -- the same
exit code as a genuine build failure or a rejected fix -- even though
nothing went wrong and no model was ever consulted. A recent benchmark
run showed this exact pattern identically across all 5 models for
CVE-2025-47183, confirming it is a deterministic corrector-path gap,
not a model-quality signal.

Route this branch through the same optional-evidence machinery the
other completed paths already use: _record_semantic_validation(None,
None, ...) naturally falls through validate_semantic_result's
existing manifest-is-None branch, since building a real
ReferenceManifest/GeneratedSnapshot is structurally impossible once
the workspace is gone. This produces an honest
NOT_EVALUATED/comparison_artifact_missing outcome (with a real
failure_class/failure_code) instead of the unreasoned legacy shim, so
it's distinguishable from a genuine rejection/failure without
fabricating comparison evidence that can't exist.

Add test_clean_apply_already_finalized_reports_honest_outcome, which
fails without the fix (failure_code is None) and passes with it.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The runner selected the per-run artifact directory by open-coding
"$artifact_data_root/results/cases/$cve_id", but
shared.paths.data_dir() appends the application component
('yocto-security-tools') to CVE_TOOLS_DATA_DIR, and
cve_agent.artifacts.RunArtifacts creates runs under
data_dir()/results/cases/<cve>. The probed path therefore never
existed: iterdir() raised FileNotFoundError, the 'exactly one run
directory' check saw zero, and artifact_dir was silently set to empty
for every run.

Both consumers of artifact_dir then degraded without any trace:

  * benchmark_artifact_outcome() was never called, so durable_summary
    stayed '-' and the exit-status override never fired -- rows kept
    the raw process exit code, which is 14 for any outcome the default
    --security-gate does not accept, including completed workflows.
  * candidate_ready stayed false, so the generated-vs-reference patch
    comparison only ran for rows whose raw exit was already 0.

In bench_20260904_165741 that left 93 of 100 rows at
diff_bucket='-'. Since filter_for_judging() only judges
minor/moderate/major/partial, the judge phase evaluated exactly one
row out of 100. The 7 rows that did get a bucket correlate perfectly
with the 7 whose raw exit status was 0, confirming candidate_ready was
never true for any row.

Derive the case root through data_dir() so the layout cannot drift
from the agent again, and report a diagnostic on stderr (appended to
the run log) naming the case root, walking the ancestor chain, and
listing the deepest directory that does exist. stdout stays the sole
channel for the resolved path, since anything printed there is
captured into artifact_dir itself.

Add two tests that execute the runner's own extracted selection
program rather than reimplementing it: one asserts it resolves a real
RunArtifacts directory (fails with the open-coded join restored), the
other asserts a miss is attributable from stderr alone.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
run_benchmark.sh compares each generated backport against the human
reference at the end of every run, while the patch is still in the OE
tree. When that comparison does not happen the row keeps
diff_bucket='-', and since filter_for_judging() only judges
minor/moderate/major/partial, the judge phase silently skips it. In
bench_20260904_165741 that left 1 judged row out of 100.

Re-running the agent to recover those rows costs the full model spend
again, and it is unnecessary: every attempt already writes its final
candidate commits to a durable host-owned artifact directory
(final-commits.patch), which survives reset_oe_tree.

Add backfill_diffs.py, which replays only the comparison step from
those artifacts. It needs no Yocto environment, no OE tree and no
model calls: the reference side comes from the bench_<cve>_*.patch
copies the run already saved, and the generated side from the artifact
directory. It writes the per-model *_differences.txt and
*_differences_diff.patch files the judge phase reads, updates
agent_results.csv in place (keeping the original as .csv.orig), and
leaves rows that already have a bucket untouched unless --force.

Bucket rules mirror the runner rather than inventing new ones: a
durable SKIPPED state has no candidate, a completed built attempt is
compared even when the release gate rejected it, and any other
non-comparable outcome (escalated, failed) keeps '-'. Artifact
provenance is validated with the same manifest binding the live runner
uses, so an unrelated directory is never consumed.

On bench_20260904_165741 this recovers 55 judgeable rows from 1. The
26 rows that stay '-' account exactly for the durable outcomes that
cannot be compared: 12 escalated, 7 with an empty artifact, 5 failed,
and 2 without a valid artifact directory.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The outcome column was empty for all 100 rows of
bench_20260904_165741. parse_agent_outcome() matches the old
'<check> <cve>: <status>' line, but cve_agent/__main__.py now prints
'<cve>: <summary_state>' (e.g. 'CVE-2024-7537: SECURITY_VERIFIED'), so
the regex never matched and every row recorded ''.

That silently disabled the safeguard the column exists for. As
bench_lib already documents, the process exit status conflates
'skipped' (exit 0, but no backport -- possibly a live vulnerability
dismissed as not-applicable) with a real success, and 'escalated'
(exit 14, the correct answer for an out-of-scope fix) with a genuine
breakage. With the column blank, generate_benchmark_report.py could
not tell them apart, and its not-applicable audit -- gated on
outcome == 'skipped' -- never ran at all.

summary_state cannot be reverse-mapped to recover it: it returns
SECURITY_REVIEW_REQUIRED for both a completed-but-review-required run
and a WorkflowStatus.ESCALATED one, which is exactly the conflation
being guarded against. The durable result.json already carries the
ResultStatus verbatim in legacy_status, so read it there instead,
following the same optional-artifact pattern count_tool_calls() uses.
The log stays a fallback for runs old enough to print the legacy line,
and an unreadable artifact yields '' rather than a guess.

Also recover the column for finished runs in backfill_diffs.py, and
keep a row's existing bucket while doing so: that comparison ran
against the real OE tree and stays authoritative, whereas the outcome
was never populated for any row. On bench_20260904_165741 this
recovers 76 conflict_resolved, 12 escalated, 5 success and 5 failed,
matching the legacy_status distribution in the artifacts exactly; the
remaining 2 have no valid artifact.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
classify_outcome() returned 'failed' for any exit_status other than
'0'. That column no longer holds a pass/fail flag: run_benchmark.sh
records the durable ResultOutcome.summary_state there whenever one
exists, so a run that completed and produced a comparable patch still
reports e.g. SECURITY_REVIEW_REQUIRED when the release gate declined
to accept it. In bench_20260904_165741 that wrote off 93 of 100 rows
as failures, 45 of which had completed and produced a patch --
including CVE-2024-7537/claude-sonnet-4.6, whose backport differs from
the human reference by 5 lines.

Read the run's disposition from the outcome column instead, which now
carries cve-agent's own ResultStatus from result.json, and reserve
'failed' for the states that really produced nothing to compare
(WORKFLOW_FAILED, HOST_INITIALIZATION_ERROR, PROVIDER_TIMEOUT,
AGENT_NO_PROGRESS, TIMEOUT, SETUP_FAILED, plus a bare non-zero exit
code when no durable outcome was recorded). Completed runs are scored
on patch equivalence, which is what run_benchmark.sh already intends by
keeping them comparable.

Add two categories rather than folding them into existing ones:

  * escalated -- the model declined to guess and asked for a human.
    bench_lib documents this as the correct answer for an out-of-scope
    fix, so scoring it identically to a crash punishes the honest
    result.
  * gate-rejected -- the host's semantic validation rejected the
    result. This can accompany a patch textually identical to the
    reference (rejection grounds such as a missing prerequisite are
    invisible to a diff), so 'equivalent' would hide a real security
    finding while 'failed' would misreport a correct patch. Four rows
    in this run are exactly that shape.

The 'broken' column in the text summary stays failed + no-patch. Rows
without an outcome column keep their previous classification, so older
result directories read unchanged.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- CVE-2025-47183 (gstreamer1.0-plugins-good) was tiered as an "easy"
  conflict-resolution case (conflict_markers: 1, exit_code: 1) in
  benchmark-roster-extended.json, but bench_20260904_165741 shows every
  model's cve-corrector run actually exits 0 with a clean cherry-pick
  apply and no conflict at all ("Clean apply (workspace already
  finalized)")
- Per bench_lib.py's own documented design, a clean exit "is not a
  resolution case at all" and belongs in the clean-apply roster instead,
  so no per-model diff/judge comparison was ever generated for it in the
  extended roster, silently excluding it from judge_results.csv
- Move the entry to benchmark-roster-clean-apply.json with the correct
  schema (phase: "clean_apply", diff_lines: 0, series_len: 1) and drop
  it from benchmark-roster-extended.json
- Update tests/benchmark/README.md's roster tables/counts (extended:
  20 -> 19 CVEs, 9 -> 8 easy; clean-apply: 6 -> 7 CVEs) and
  test_benchmark_roster.py's EXPECTED_SIZES to match

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- _extract_files_touched() only recognized diff --git a/<path> b/<path>
  headers, so a reference patch using plain diffutils -p / Index: style
  (no diff --git line, --- / +++ paths carrying a version-numbered
  tarball directory instead of git's a/ / b/) returned an empty file
  set
- This made compare_patches_detailed() misreport every file the
  reference patch touches as "missing in generated" relative to a
  generated patch that in fact touches the exact same file, forcing
  classify_diff_bucket() to bucket the run as file-mismatch and skip it
  entirely from judge_results.csv
- Confirmed against the real CVE-2025-1176 (binutils) reference patch
  from bench_20260904_165741: before the fix, _extract_files_touched()
  returned an empty set for
  bench_CVE-2025-1176_CVE-2025-1176.patch (an Index:-style patch
  imported from Ubuntu's debian/patches/); after the fix it correctly
  returns {'bfd/elflink.c'}, matching the generated patch's diff --git
  header
- Fall back to the +++ header for patches without a diff --git line,
  stripping a/ / b/ when present or otherwise the leading tarball
  directory component (patch -p1 semantics); skip /dev/null so a
  deleted file's +++ header isn't reported as a bogus path
- Add regression tests for the Index:-style case and for /dev/null
  headers in tests/integration/test_patch_compare.py

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
@danielturull
danielturull merged commit 510ad1e into main Sep 7, 2026
8 checks passed
@danielturull
danielturull deleted the feature/openai+fixes branch September 7, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants