Problem
Every contributor-scoped anti-abuse control keys on the mutable GitHub login. The immutable numeric
user id is present in every webhook payload and is discarded at content ingest, so a contributor clears
all of these controls at once by renaming their account — a free, instantaneous, self-service action.
findBlacklistEntry (packages/loopover-engine/src/settings/contributor-blacklist.ts:67-71):
const key = login.toLowerCase();
return (entries ?? []).find((entry) => entry.login.toLowerCase() === key) ?? null;
ContributorBlacklistEntry has no id field at all — the module validates entries against a GitHub
login regex (:12) and nothing else.
The ingest side matches: PR/issue upsert writes only authorLogin: pr.user?.login, and src/db/schema.ts
has no author-id column on pull_requests, issues, recent_merged_pull_requests, or the moderation
tally. Note the id is not universally unavailable — src/auth/security.ts:316 and
src/orb/oauth.ts:100,112 both capture user.id on the OAuth/enrollment paths. It is specifically the
webhook content-ingest path that drops it.
Controls that consequently reset on rename:
| control |
site |
| blacklist deterministic close (fires ahead of all merit/CI/AI analysis) |
contributor-blacklist.ts:67 |
| open-PR / open-item caps |
src/db/repositories.ts:4602,4616 |
| moderation ban tally |
src/db/repositories.ts:2977 ("persist by login") |
| review-nag ping counter |
countRecentAuditEventsForActorInRepo(env, commenter, …) |
autoCloseExemptLogins |
settings resolution |
Trigger
A contributor blacklisted as spammer99 renames to spammer99x. GitHub carries the account, its PRs and
its history across the rename. The next PR arrives with user.login = "spammer99x"; findBlacklistEntry
returns null; the deterministic blacklist close never fires; the PR re-enters full merit review and is
eligible for auto-merge. In the same move their open-PR count resets to 0 (cap bypassed), their
moderation ban tally resets to 0, and their review-nag budget resets.
Impact
Wrong-merge path. The one production anti-abuse control documented as zero-hallucination and immune to the
close-precision breaker is bypassable by the attacker's own action, and the bypass simultaneously clears
every rate limit that would otherwise slow a repeat offender. On a repo whose merges drive upstream
rewards, this is the highest-value control to defeat and the cheapest to defeat.
Dedup
Distinct from #9079 (miner detection keyed on the renameable login). There, githubId is already
fetched and merely discarded at the comparison — the fix is a one-line comparison change. Here ORB has
no immutable contributor identity anywhere in the content schema: user.id is dropped at the webhook
boundary, so the fix is an ingest + schema change plus an optional id on the blacklist entry. Different
subsystem, different remediation. They should land together, since both stem from the same missing column.
Requirements
- Persist
author_github_id on pull_requests and issues at upsert (contiguous migrations/NNNN_*.sql),
populated from the webhook payload's user.id.
- Add an optional
githubId to ContributorBlacklistEntry and match on id-when-present ∪ login, so
existing login-only entries keep working and new entries can be rename-proof.
- Move the open-item caps, the moderation tally, and the nag counter to id-when-present.
- Backfill is best-effort — document which historical rows cannot be attributed rather than guessing.
- A rename should be observable: when a known id appears under a new login, record an audit event.
Test Coverage Requirements
99%+ patch coverage, branch-counted. Both arms of id-present / id-absent matching, plus a regression test
that a blacklisted id under a new login is still matched.
Links & Resources
Boundaries
Identity keying for contributor-scoped controls. No change to blacklist semantics, cap thresholds, or the
close copy.
maintainer-only — anti-abuse and schema authority.
Problem
Every contributor-scoped anti-abuse control keys on the mutable GitHub login. The immutable numeric
user id is present in every webhook payload and is discarded at content ingest, so a contributor clears
all of these controls at once by renaming their account — a free, instantaneous, self-service action.
findBlacklistEntry(packages/loopover-engine/src/settings/contributor-blacklist.ts:67-71):ContributorBlacklistEntryhas no id field at all — the module validates entries against a GitHublogin regex (
:12) and nothing else.The ingest side matches: PR/issue upsert writes only
authorLogin: pr.user?.login, andsrc/db/schema.tshas no author-id column on
pull_requests,issues,recent_merged_pull_requests, or the moderationtally. Note the id is not universally unavailable —
src/auth/security.ts:316andsrc/orb/oauth.ts:100,112both captureuser.idon the OAuth/enrollment paths. It is specifically thewebhook content-ingest path that drops it.
Controls that consequently reset on rename:
contributor-blacklist.ts:67src/db/repositories.ts:4602,4616src/db/repositories.ts:2977("persist by login")countRecentAuditEventsForActorInRepo(env, commenter, …)autoCloseExemptLoginsTrigger
A contributor blacklisted as
spammer99renames tospammer99x. GitHub carries the account, its PRs andits history across the rename. The next PR arrives with
user.login = "spammer99x";findBlacklistEntryreturns null; the deterministic blacklist close never fires; the PR re-enters full merit review and is
eligible for auto-merge. In the same move their open-PR count resets to 0 (cap bypassed), their
moderation ban tally resets to 0, and their review-nag budget resets.
Impact
Wrong-merge path. The one production anti-abuse control documented as zero-hallucination and immune to the
close-precision breaker is bypassable by the attacker's own action, and the bypass simultaneously clears
every rate limit that would otherwise slow a repeat offender. On a repo whose merges drive upstream
rewards, this is the highest-value control to defeat and the cheapest to defeat.
Dedup
Distinct from #9079 (miner detection keyed on the renameable login). There,
githubIdis alreadyfetched and merely discarded at the comparison — the fix is a one-line comparison change. Here ORB has
no immutable contributor identity anywhere in the content schema:
user.idis dropped at the webhookboundary, so the fix is an ingest + schema change plus an optional id on the blacklist entry. Different
subsystem, different remediation. They should land together, since both stem from the same missing column.
Requirements
author_github_idonpull_requestsandissuesat upsert (contiguousmigrations/NNNN_*.sql),populated from the webhook payload's
user.id.githubIdtoContributorBlacklistEntryand match on id-when-present ∪ login, soexisting login-only entries keep working and new entries can be rename-proof.
Test Coverage Requirements
99%+ patch coverage, branch-counted. Both arms of id-present / id-absent matching, plus a regression test
that a blacklisted id under a new login is still matched.
Links & Resources
packages/loopover-engine/src/settings/contributor-blacklist.ts~12, ~67-71;src/db/schema.ts~345, 520, 542, 776;src/db/repositories.ts~4602, 4616, 2977;src/queue/processors.ts~2697-2711verifyInstallationAdminpath insrc/orb/oauth.ts~100 as thein-repo example of binding to immutable
account_idBoundaries
Identity keying for contributor-scoped controls. No change to blacklist semantics, cap thresholds, or the
close copy.
maintainer-only — anti-abuse and schema authority.