Skip to content

fix(reporting): serialize the duplicate check so concurrent agents cannot file a finding twice - #1469

Open
samirhvbr wants to merge 3 commits into
usestrix:mainfrom
samirhvbr:fix/dedupe-concurrent-filing
Open

samirhvbr wants to merge 3 commits into
usestrix:mainfrom
samirhvbr:fix/dedupe-concurrent-filing

Conversation

@samirhvbr

@samirhvbr samirhvbr commented Oct 6, 2026 •

Copy link
Copy Markdown

Concurrent agents can both store the same finding while the duplicate check awaits its model response. Each compares against a snapshot that does not yet contain the other filing.

Hold a per-run ReportState.dedupe_lock across reading stored findings, checking for duplicates and inserting the accepted report. Both create_vulnerability_report and create_dependency_report use the same lock.

Dependency checks normally resolve synchronously from CVE/package identity. However, an existing same-CVE report without package metadata can send the check through the model. That fallback has the same race and now receives the same protection. The structured package-identity fast path remains model-free. Distinct findings remain accepted, and an exceptional check releases the lock.

Fixes #627.

Validation on macOS/Apple Silicon, Python 3.12.14, with isolated HOME/configuration:

  • Regression tests drive the real reporting helpers and real check_duplicate; only the model is replaced with a synthetic response that yields execution.
  • The dependency fallback regression fails before the follow-up correction: both concurrent filings return success. Afterward exactly one is stored and the other identifies it as the duplicate.
  • A control verifies that package-identity deduplication still needs no model call.
  • 182 tests passed across reporting races/fields/tools, list reports, dedupe, report state and report writing.
  • An experimental merge with current upstream 6b7ae2eb applied cleanly and passed the same 182 tests.
  • Ruff lint/format, Mypy and Bandit passed on the changed code; git diff --check passed.

The trade-off is that filings in one run wait for an in-flight duplicate check. The lock is scoped to the report state, not shared across separate runs.

The complete repository suite, real model calls and live scans were not exercised. Remote CI still requires maintainer approval.

Prepared with AI assistance.

…nnot file a finding twice

The duplicate check awaits an LLM call and compares the candidate with the
findings that were stored when it started. Two agents filing the same finding
at the same moment each saw a list without the other's report, passed the
check and both stored it. Reproduced against the real create functions with a
slow check: three concurrent identical filings were stored three times, while
a sequential fourth was correctly rejected.

Hold a per-run asyncio lock across read existing -> check -> add in both
create_vulnerability_report and create_dependency_report. Distinct findings
still go through, one duplicate check at a time. The candidate and field dicts
are built before the lock (they do not depend on the stored list), so the
diff is mostly indentation: `git diff -w` is 13 lines.

Fixes usestrix#627

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds concurrency control to the duplicate-finding check.

The code appears safe to merge, but the race tests should cover a path where the real duplicate check pauses.

Findings

  1. P2 Race tests miss real waits ▶
Fix with agent prompt
### Issue 1
tests/test_reporting_dedupe_race.py:45-49
Each test starts with no stored findings, but `_slow_check` always pauses. The real `check_duplicate` returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Both report creation paths now check saved findings and add new ones one at a time, so concurrent matching filings don’t both get stored.

  • Concurrent matching findings get saved only once.

Reviews (1) · Last reviewed commit: "fix(reporting): serialize the duplicate ..."

Comment thread tests/test_reporting_dedupe_race.py Outdated
Comment on lines +45 to +49
async def _slow_check(candidate: dict[str, Any], existing: list[dict[str, Any]]) -> dict[str, Any]:
"""Stand in for the real check: it judges the candidate against the snapshot it is handed."""
await asyncio.sleep(0.05) # the real one is a model call
for report in existing:
if report.get("title") == candidate["title"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Race tests miss real waits

Each test starts with no stored findings, but _slow_check always pauses. The real check_duplicate returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_reporting_dedupe_race.py
Line: 45-49

Comment:
**Race tests miss real waits**

Each test starts with no stored findings, but `_slow_check` always pauses. The real `check_duplicate` returns right away for an empty list and for matching dependency CVE/package pairs. These tests could pass even if the race returns on a path that waits for the model. Seed an unrelated finding in the vulnerability test and test a dependency case that reaches the model check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

samirhvbr pushed a commit to samirhvbr/strix that referenced this pull request Oct 6, 2026
Bump to 1.7.0+shvia.22 and add the FORK.md row plus the four contributions
(usestrix#1469 dedupe lock, usestrix#1470 whitebox repository targets, usestrix#1471 test isolation from
the real home, usestrix#1472 trust fields in the PDF report). CLAUDE.md now says the suite
no longer writes the real ~/.strix, and that `import litellm` loads the current
directory's .env unless LITELLM_MODE=PRODUCTION.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb
Resolves the conflicts with the retry and reporting changes that landed on main since
this branch was opened. The resulting tree is the PR rebased onto main, tested there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb

This branch has not been deployed

No deployments
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.

[BUG]Concurrent agents can bypass deduplication and file the same vulnerability twice

1 participant