Skip to content

fix(agent-actions): live-verify + installation-scope the global contributor cap - #2689

Closed
JSONbored wants to merge 1 commit into
mainfrom
fix/global-contributor-cap-live-verify
Closed

fix(agent-actions): live-verify + installation-scope the global contributor cap#2689
JSONbored wants to merge 1 commit into
mainfrom
fix/global-contributor-cap-live-verify

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Adversarial review of the already-merged #2678 (issue #2562) found two confirmed, empirically-verified bugs, and a third surfaced while porting the fix:

  • No live verification before an irreversible close: countOpenItemsForAuthorAcrossRepos trusted the raw D1 cache directly. The sibling per-repo issue-cap path (maybeCloseIssueOverContributorCap) already live-verifies each sibling via fetchLiveIssueState before trusting it toward a close, specifically because the stored cache can lag GitHub. The install-wide path skipped this entirely.
  • itemKind mislabeling: contributorCapMatch.itemKind was hardcoded to "pull requests" (PR call site) or "issues" (issue call site) even though the count sums PRs + issues together — a mixed-kind contributor got a close message with a wrong noun/count.
  • No installation scoping: the aggregate query had no installationId filter — it summed across the ENTIRE D1 database regardless of which GitHub App installation each repo belongs to. A D1 instance serving more than one installation could wrongly count a contributor's unrelated-installation activity toward a close here.

Replaced countOpenItemsForAuthorAcrossRepos (a count) with listOpenItemsForAuthorAcrossInstall (rows, installation-scoped), added verifiedGlobalOpenItemCount which live-confirms every OTHER counted item (bounded worker-pool fan-out, concurrency 10, to avoid an unbounded burst against the installation's rate limit) before trusting it toward the cap, and widened itemKind to include "pull requests and issues" for the install-wide case.

Addresses gate-review findings on #2678.

Test plan

  • npm run typecheck
  • npx vitest run test/unit/global-contributor-cap.test.ts test/unit/queue.test.ts — 395/395 pass, including all 9 existing install-wide cap webhook-flow tests (updated to register repos under the calling installation, matching the new installation-scoping requirement) and 3 new/updated listOpenItemsForAuthorAcrossInstall tests covering cross-installation isolation and list-truncation observability

…ibutor cap

Fixes two confirmed bugs in the already-merged #2678 (issue #2562):
the install-wide open-item count trusted the raw D1 cache with no live
verification before an irreversible close (the sibling per-repo issue
cap already avoids this exact failure mode), and itemKind was
hardcoded to a single kind even though the count sums PRs + issues,
producing a factually wrong close-comment noun for a mixed-kind
contributor. Also scopes the aggregate query to the calling
installation's own repo set -- an unscoped cross-database query could
wrongly count a contributor's activity on a totally unrelated
installation's repos toward a close here.

Addresses gate-review findings on #2678.
@dosubot dosubot Bot added the size:L label Jul 3, 2026

@superagent-security superagent-security Bot left a comment

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.

Superagent found 1 security concern(s).

Comment thread src/queue/processors.ts
const token = await createInstallationToken(env, installationId).catch(() => undefined);
const liveToken = token ?? env.GITHUB_PUBLIC_TOKEN;
const admissionKey = githubAdmissionKeyForToken(env, installationId, liveToken);
const confirmedOpen = await mapWithConcurrency(otherRows, GLOBAL_OPEN_ITEM_LIVE_CHECK_CONCURRENCY, (row) =>

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 verifiedGlobalOpenItemCount can exhaust GitHub rate limits via unbounded live verification fan-out

A single webhook can trigger up to 40,000 GitHub API calls, exhausting the installation's rate limit.

Short-circuit live verification once globalCap confirmed items are reached.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="src/queue/processors.ts">
<violation number="1" location="src/queue/processors.ts:4008">
<priority>P2</priority>
<title>verifiedGlobalOpenItemCount can exhaust GitHub rate limits via unbounded live verification fan-out</title>
<evidence>verifiedGlobalOpenItemCount lists all open items for an author across an installation (up to 20,000 PRs + 20,000 issues from listOpenItemsForAuthorAcrossInstall) and then calls mapWithConcurrency(otherRows, GLOBAL_OPEN_ITEM_LIVE_CHECK_CONCURRENCY, ...) to live-verify every single item via GitHub API, even though only globalCap items need to be confirmed. With concurrency 10 and up to 39,999 other rows, a single webhook can consume thousands of API calls and occupy the worker for minutes, exhausting the installation's rate limit and denying service to other repos.</evidence>
<recommendation>Short-circuit the live verification once globalCap confirmed-open items have been found, instead of checking every row. Alternatively, cap the total number of live checks to a small multiple of globalCap, or cache live-verification results for a short TTL.</recommendation>
</violation>
</file>

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jul 3, 2026
@JSONbored JSONbored self-assigned this Jul 3, 2026
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.87234% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 96.13%. Comparing base (dc9cbde) to head (05d5c9a).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 96.55% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2689      +/-   ##
==========================================
- Coverage   96.13%   96.13%   -0.01%     
==========================================
  Files         239      239              
  Lines       26777    26817      +40     
  Branches     9719     9727       +8     
==========================================
+ Hits        25743    25781      +38     
  Misses        424      424              
- Partials      610      612       +2     
Files with missing lines Coverage Δ
src/db/repositories.ts 96.64% <100.00%> (+0.03%) ⬆️
src/settings/agent-actions.ts 94.32% <ø> (ø)
src/queue/processors.ts 92.72% <96.55%> (-0.02%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JSONbored

Copy link
Copy Markdown
Owner Author

Superseded by a fresh PR from current main — this branch predates an installation-scoping fix that landed concurrently on main and conflicts here.

@JSONbored JSONbored closed this Jul 3, 2026
@JSONbored
JSONbored deleted the fix/global-contributor-cap-live-verify branch July 4, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:flagged PR flagged for review by security analysis.

Development

Successfully merging this pull request may close these issues.

1 participant