Skip to content

fix(review): default cross-issue copycat/reward-farming dedupe on for reward-eligible repos - #9226

Merged
JSONbored merged 6 commits into
mainfrom
fix/9033-copycat-reward-farming
Jul 27, 2026
Merged

fix(review): default cross-issue copycat/reward-farming dedupe on for reward-eligible repos#9226
JSONbored merged 6 commits into
mainfrom
fix/9033-copycat-reward-farming

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Two colluding accounts (or one attacker with two identities) could file near-identical fixes under different linked issues and both merge — double reward for one piece of work.

  • Copycat containment-similarity detection is now default-on for reward-eligible (subnet-registered) repos, instead of defaulting off.
  • Cross-issue copycat containment matches now feed the duplicate-cluster election, not just a single-PR advisory finding — a PR whose content matches an already-open or already-merged sibling under a DIFFERENT linked issue is now treated as the same cluster for winner-election purposes.
  • Raised and paginated the copycat candidate bound, ranking merged prior art by changed-file overlap first so the expensive patch-content fetch stays bounded.
  • Added an end-to-end regression test for the exact scenario in the issue: two PRs, different linked issues, near-identical content, on a reward-eligible repo.

Test plan

Closes #9033

…9033)

copycatGateMode's absent-value fallback was a flat "off" for every repo
(config-as-code only, no DB column), so the deterministic containment
engine never ran unless a repo explicitly opted in via .loopover.yml --
a direct economic leak for a reward-eligible repo, since colluding
accounts could file near-identical fixes and both merge undetected.

resolveEffectiveCopycatGateMode resolves an unset mode to "warn" for a
subnet-registered (reward-eligible) repo instead, so the engine runs and
persists copycatScore/copycatMatchedPullNumber -- the signal the
duplicate-cluster election needs. An explicit .loopover.yml value,
including an explicit "off", always wins over this default.
…election (#9033)

The duplicate-winner election (queue/duplicate-detection.ts) keyed
entirely on shared linkedIssues overlap, so two PRs citing different
issues -- one a near-identical copy of the other -- were never even
considered duplicate-cluster candidates, letting both merge and both
earn rewards independently.

resolveCopycatDuplicateSibling (src/signals/copycat-duplicate.ts)
bridges a PR's already-persisted copycat containment match into
linkedIssueDuplicatePullRequestRecordsForGate and
reconcileLiveDuplicateSiblings, so a cross-issue content match above
the configured threshold now feeds the SAME winner-election, close-
count, and live-staleness-reconciliation machinery a same-linked-issue
duplicate already does. Scoped to still-open siblings only -- a match
against an already-merged PR is left to copycatGateMode's own single-PR
warn/label/block actuation. Every new parameter is optional and
defaults to "never contributes a copycat sibling", so no existing
caller's behavior changes.
… overlap (#9033)

The duplicate_pr_risk gate finding -- the mechanism that actually holds
or blocks a PR as a duplicate -- keyed strictly on shared linkedIssues
overlap, so a cross-issue copycat containment match never surfaced
here even after the duplicate-cluster election learned about it: the
election could name a winner, but the loser's gate never actually
failed on it.

addPullRequestFindings now folds a PR's own persisted copycat
containment match into the same overlap set a shared linked issue
produces, treating it as corroborated by construction (a threshold-
cleared content match is stronger evidence than a bare changed-file
overlap). This is what makes the loser in a cross-issue reward-farming
attempt actually get held/closed instead of merging independently.
…art by overlap (#9033)

MAX_COPYCAT_CANDIDATES (25) capped how many prior-art candidates got
their full patch content fetched and scored -- on an active repo, a
copycat's actual match could sit past that cutoff and never get
compared at all.

Raised to 50 (still a bounded, cheap, single-repo-scoped set of D1
reads, not external API calls). The recently-merged phase also now
ranks its path-overlapping candidates by OVERLAP COUNT descending
before spending its share of the budget, rather than merely the most
recently merged ones that happen to overlap at all -- a stronger prior
signal of a genuine match, at no extra fetch cost since changedFiles is
already loaded.
…ssion (#9033)

Exercises the full confirmed scenario from #9033: two PRs on a reward-
eligible repo, different linked issues, near-identical added code.
Covers reward-eligible settings resolution, the duplicate-cluster
helpers, the gate finding/conclusion, and the actual maintenance
disposition -- proving the later cross-issue copycat PR is held for
manual review (never merges independently) while the earlier claimant
merges untouched on its own merits.

Also documents an important existing interaction: #9129 already
downgrades any gate failure caused solely by duplicate_pr_risk (even a
corroborated one) to a neutral hold rather than an outright close, so
the cross-issue copycat match now goes through that identical,
precision-safe mechanism instead of bypassing it.
…te-decision twin (#9033)

src/rules/advisory.ts's evaluateGateCheckCore gained a host-only
cross-issue copycat corroboration branch (the duplicate-cluster
election fix). It intentionally does not mirror into
packages/loopover-engine/src/advisory/gate-advisory.ts -- the engine
twin is deliberately kept slim (no signals/engine, no
PullRequestRecord.copycatScore) so the miner/mcp CLI packages never
drag that dependency graph in.

Bumps the engine package version (the guard's own documented escape
hatch for a one-sided fix, same shape as the precedent manual bump in
6aacf08), syncing package-lock.json, the miner's
expected-engine.version pin, and the release-please manifest alongside
it.
@JSONbored JSONbored self-assigned this Jul 27, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@github-actions

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (93daac9) and head (03ef003) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 03ef003 Commit Preview URL

Branch Preview URL
Jul 27 2026, 08:04 AM

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@JSONbored
JSONbored merged commit b251912 into main Jul 27, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the fix/9033-copycat-reward-farming branch July 27, 2026 08:08
@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
22719 4 22715 21
View the top 3 failed test(s) by shortest run time
test/integration/orb-relay.test.ts > POST /v1/orb/relay/pull > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.00871s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:1160:24
test/integration/orb-onboarding.test.ts > Central Orb installation registry routes (/v1/internal/orb/installations) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.0194s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-onboarding.test.ts:57:14
test/integration/orb-ingest.test.ts > Orb instance registry routes (/v1/internal/orb/instances) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.0331s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-ingest.test.ts:427:14
test/integration/orb-relay.test.ts > POST /v1/orb/relay/register > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.121s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:155:24

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orb(anti-abuse): duplicate/copycat reward-farming is off by default and only matches on linked issue

1 participant