Stop the GitHub triage relay amplifying issue spam - #265
Merged
Conversation
Two throwaway accounts filed 23 SEO backlink issues on CopilotKit/CopilotKit on 2026-09-10/11. Outpost relayed each ~3.3 KB body verbatim into search-docs and search-code on mcp.copilotkit.ai within ~4s of creation and answered them publicly, producing 46 query_log rows that then ranked in Top Queries and fed the weekly Notion report and the monthly gap-analysis LLM prompt. Three levers: - A link-spam gate in the issues.opened webhook, BEFORE a ticket exists, so no AI job is enqueued and nothing is relayed or posted. Measured on the full issue history of CopilotKit/CopilotKit: 23/23 spam, 0/1264 legitimate. - A hard cap on the MCP search query. Content-independent, so it bounds the next campaign too; the generator still sees the full body. - X-Pathfinder-Source on the MCP initialize request, so this relay's traffic is attributable and excludable downstream.
Mutation-checked: neutering isLikelySpamIssue fails 3 of these, neutering capQuery and the source header fails 6 in pathfinder.test.ts.
CI format-checks every changed file, and these were already non-conforming on main — so the pre-existing violations have to go with them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What happened
On 2026-09-10/11 two throwaway accounts (
sarahnicholas1327-lgtm,kaylaford203-beep) filed 23 SEO backlink issues onCopilotKit/CopilotKit— ~3.3 KB of marketing prose each, titled variants of "Why 1Rank.app Belongs in Your SEO Growth Stack", with a dozen-odd links to1rank.app/zagfro.com. All are now closed.Outpost's GitHub triage relay forwarded every one of those bodies verbatim into
search-docs+search-codeonmcp.copilotkit.ai, within ~4 seconds of issue creation, and then answered the issue publicly ascopilotkit-outpost-bot[bot]. Confirmed against Pathfinder'squery_log:46
query_logrows, one IP (152.55.178.126, Railway), UAnode, onesearch-docs+ onesearch-codeper issue. Those rows then ranked in Pathfinder's Top Queries panel, went into the weekly search report's Notion table, and were eligible as cluster representatives in the monthly gap-analysis LLM prompt — 3.3 KB of third-party marketing copy with live backlinks, published to Notion via a model prompt. That last one is the part worth fixing properly.The spammer wrote once and got a machine-generated reply, 46 rows of retrieval traffic, and a foothold in three of our reports for free. That is amplification, and it is ours.
The change
Three levers. Only the first is content-aware.
1. Drop link-spam in the
issues.openedwebhook, before a ticket existsapps/github-app/src/lib/spam-filter.ts. The gate sits ahead ofInboundHandlerdeliberately: creating the ticket is what enqueues the AI job, and that job is what does the relaying and the posting. A filter further down would already have paid for it.All four conditions must hold:
OWNER/MEMBER/COLLABORATOR/CONTRIBUTOR(a spam account isNONEby construction; a maintainer or prior contributor can never be silenced by this gate)localhostdon't count)Measured against the full issue history of
CopilotKit/CopilotKit— 1,264 non-spam issues plus all 23 known spam issues:len >= 3000 AND urls >= 6A length-and-URL rule alone is not good enough: it eats real bug reports (#2667 is 40 KB with 16 URLs, #3510 is 13 KB with 26). The two terms that buy the separation describe an advertisement rather than a report — no code in it, and the links point over and over at the same third-party host. A stack trace or a repro has code; a backlink campaign does not, because the links are the payload.
Honest about scope: this is fitted on one campaign and one repo's history. It is a discriminator, not a general spam classifier.
2. Cap the MCP search query (the durable one)
capQuery()inpackages/outpost/ai/src/pathfinder.ts, default 1000 chars,PATHFINDER_MAX_QUERY_CHARSto override. Content-independent, so it bounds the next campaign whatever it advertises.A retrieval query is an embedding input, not a transcript — the generator still sees the full body, only the search string is capped. Context from 7 days of
query_log: the longest query from any client that is not a relay is 194 characters; these spam bodies ran 3,304–3,631 and scored 0.33–0.43 cosine for it, so the long tail was buying nothing. The head is kept (that is where the question lives) and the cut pulls back to a word boundary if one is in the last 15%.3.
X-Pathfinder-Source: outposton the MCPinitializerequestSo this relay's traffic is attributable and, more usefully, excludable downstream. Pathfinder captures this header once, at session initialisation, and closes over it for the session's lifetime — setting it on an individual
tools/callsilently does nothing, so it rides oninitializeand there is a test asserting exactly that asymmetry.Value is
outpost(envPATHFINDER_SOURCE) rather thangithub-triage: this client also serves the Discord, Slack and Teams bots, and all of it is relay traffic that should be excluded together. Tagging a Discord query "github-triage" would be a lie.Red / green
Harness (not committed): a local HTTP server speaking MCP Streamable-HTTP, the real
PathfinderClientpointed at it, fed the real bodies of all 23 spam issues and all 1,264 other issues pulled live from the GitHub API.RED (this branch, fix reverted):
GREEN (this branch):
Note C: a 62 KB legitimate bug report from a first-time reporter is still relayed and still answered — it is only its search string that shrinks.
Mutation-tested
The new tests are not vacuous. Neutering
isLikelySpamIssuetoreturn falsefails 3 of them; neuteringcapQueryand dropping the source header fails 6 inpathfinder.test.ts.Local gate
pnpm lint,pnpm typecheck,pnpm build,pnpm test(2,287 passed), andprettier --checkon every changed file — all green. One of the three commits is pure prettier: these files were already non-conforming onmain, and CI format-checks every changed file, so the pre-existing violations had to go with them.Not touched
apps/github-app/railway.tomland the service's Railway config path are untouched, as is anything underdeploy/. No env var is required for this to work — both new knobs have defaults.Follow-up, not in this PR
A parallel change adds the Pathfinder-side exclusion so relay traffic (now identifiable by
X-Pathfinder-Source) stops ranking in Top Queries, the weekly report and the gap-analysis prompt. That is the belt to this PR's braces: it protects those surfaces regardless of what the relay forwards.