fix(http): name yolocoder in User-Agent and show a refused preselection (ENG-3022) - #11
Open
lucas-koontz wants to merge 2 commits into
Open
lucas-koontz wants to merge 2 commits into
lucas-koontz wants to merge 2 commits into
Conversation
…on (ENG-3022) Lucas Koontz, for ENG-3022 "Stop free Jev floods from taking down the shared prod database (2026-09-26 incident)". Every outbound request now goes through internal/httpclient, whose transport sets "User-Agent: yolocoder/<version> (+https://github.com/mindsdb/yolocoder)". Before this, the /v1/decisions file chooser, the coding calls, model listing, the dialect probe, sign-in and the self-updater all sent Go's default Go-http-client/1.1 or Go-http-client/2.0, which is also what the Jev flood fleet sends. The edge can now block that default without blocking yolocoder. chooseFiles now prints "file preselection skipped: decisions returned <status>" on the turn's trail when /v1/decisions answers with a non-2xx status. A block or a revoked key used to cost the preselection silently on every turn. A test walks the source tree and fails when non-test code sends through http.DefaultClient or builds its own http.Client. Refs: ENG-3022
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No unresolved review issues were identified.
Review effort: Lite
Findings: None
What changed in this PR
Updates outbound requests to identify as yolocoder/<version> and reports refused file-preselection responses without interrupting turns.
Changes:
- Added shared User-Agent handling across HTTP/2, redirects, provider, auth, and update calls.
- Logged non-2xx preselection responses while preserving fallback behavior.
- Added comprehensive tests and documentation.
| File | Description |
|---|---|
README.md |
Documents User-Agent behavior and preselection failures. |
internal/update/update.go |
Routes update requests through the shared client. |
internal/update/update_test.go |
Tests updater User-Agent behavior. |
internal/httpclient/httpclient.go |
Implements the shared User-Agent transport. |
internal/httpclient/httpclient_test.go |
Tests transport, redirects, HTTP/2, and request immutability. |
internal/auth/auth.go |
Routes sign-in requests through the shared client. |
internal/auth/auth_test.go |
Tests sign-in User-Agent behavior. |
internal/agent/user_agent_test.go |
Tests User-Agent coverage across provider calls. |
internal/agent/edit_router.go |
Removes the obsolete per-call User-Agent. |
internal/agent/decide.go |
Reports non-2xx preselection responses. |
internal/agent/decide_test.go |
Tests preselection failure reporting and continuation. |
internal/agent/client.go |
Routes client and model requests through the shared client. |
internal/agent/chat.go |
Routes dialect probes through the shared client. |
internal/agent/agent.go |
Passes progress reporting into preselection. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ENG-3022) Lucas Koontz, for ENG-3022 "Stop free Jev floods from taking down the shared prod database (2026-09-26 incident)". Review round on #11. chooseFiles now also puts a line on the trail when /v1/decisions runs out of time ("decisions timed out after 6s") or cannot be reached ("decisions unreachable"). The reason given for keeping these quiet, that a user cancel ends the call, had no case behind it: every turn runs on context.Background(), so a stalled endpoint cost each turn its preselection with nothing on screen. A call whose turn context has already ended still prints nothing. The status in the skip line is now built from the code and Go's name for it, so Cloudflare's 52x codes read "522" rather than "522 " and a server's own reason phrase never reaches the terminal. The line after the step reads "file choice" instead of "chose files", which contradicted a skip line printed just above it. TestNoCodeBypassesTheSharedClient now reads the syntax tree instead of matching text. It catches http.DefaultTransport, new(http.Client), a zero-value or literal http.Client or http.Transport, and aliased or dot imports of net/http, and it no longer flags comments, strings or a struct field named http. It exempts two exact files, the client itself and the --web preview proxy, instead of any directory named httpclient. A table test pins each form. New tests pin that the shared client keeps the caller's deadline, that a shrug and a 2xx cut off mid-body print no skip line, and the timeout, unreachable and ended-turn cases above. The README scopes "every request" to provider, sign-in and update calls, names the two sends that sit outside the client, lists what the guard catches, and says preselection runs only in folders of up to 80 mapped files. Refs: ENG-3022
mindsdb-devops
approved these changes
Sep 28, 2026
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.
User story
As a MindsHub on-call engineer blocking the Jev flood at the Cloudflare edge
I want yolocoder to name itself in every request it sends
So that the edge rule that blocks Go's default user agent on
api.mindshub.ai/v1/decisionsstops the fleet without breaking our own coding agentWhy this matters
A bot fleet flooded
api.mindshub.ai/v1/decisionson 2026-09-26 and took the shared prod database down. mindsdb/terraform#227 blocked the fleet by its exact user agent,Go-http-client/1.1, and about 70 minutes later the fleet switched toGo-http-client/2.0and the floods came back. mindsdb/terraform#231 widened the rule to everyGo-http-client/agent on/v1/decisions. It merged on 2026-09-27 at 11:17 UTC and is live: aGo-http-client/2.0request to that path gets a 403 at the edge.yolocoder's file chooser never set a User-Agent, so it sends
Go-http-client/2.0too, and the edge now refuses it. The chooser also swallowed every failure. So right now every installed build with/preselecton loses its file preselection on every turn, with nothing on screen. This PR makes yolocoder name itself, which ends that at each user's next launch, and makes a refused, stalled or unreachable call say so on the trail.What happens today
sequenceDiagram participant Fleet as Flood fleet participant Yolo as yolocoder participant Edge as Cloudflare edge participant Jev as /v1/decisions Fleet->>Edge: POST with User-Agent Go-http-client/2.0 Edge-->>Fleet: 403 from the Go-http-client/ prefix rule Yolo->>Edge: POST with User-Agent Go-http-client/2.0 Edge-->>Yolo: 403 from the same rule Note over Jev: neither request arrives Note over Yolo: the chooser returns nil<br/>and prints nothingWhat should happen
sequenceDiagram participant Fleet as Flood fleet participant Yolo as yolocoder participant Edge as Cloudflare edge participant Jev as /v1/decisions Fleet->>Edge: POST with User-Agent Go-http-client/2.0 Edge-->>Fleet: 403 from the Go-http-client/ prefix rule Yolo->>Edge: POST with User-Agent yolocoder/main Note over Edge: the rule stops Go's default agents,<br/>the fleet included, and not yolocoder Edge->>Jev: yolocoder passes Note over Yolo: a refused, stalled or unreachable call<br/>prints file preselection skippedAcceptance criteria
User-Agent: yolocoder/<version> (+https://github.com/mindsdb/yolocoder), where<version>is what the build stamped. Pinned byTestEveryProviderCallNamesYoloCoder,TestSignInCallsNameYoloCoderandTestTheLaunchCheckNamesYoloCoder.Go-http-client/2.0(TestTheAgentRidesOnHTTP2).--webpreview proxy useshttp.DefaultClient,http.DefaultTransport,http.Get/Head/Post/PostForm, or makes its ownhttp.Clientorhttp.Transportvalue, including through an aliased or dot import (TestNoCodeBypassesTheSharedClient). Each form, and the comments, strings andhttp-named fields it must not flag, is pinned byTestTheGuardSeesEveryBypassAndNothingElse./preselecton, a non-2xx reply from/v1/decisionsprints exactly one trail line,file preselection skipped: decisions returned <code> <name>, and the turn still finishes (TestABlockedDecisionsCallIsOnTheTrailAndTheTurnCarriesOn). A code Go has no name for, such as Cloudflare's522, prints as the bare code (TestAnythingGoingWrongJustChoosesNothing).file preselection skipped: decisions timed out after 6s, a call that cannot connect printsfile preselection skipped: decisions unreachable, and a call whose turn context has already ended prints nothing (TestAStalledOrUnreachableDecisionsCallIsOnTheTrail).TestPreselectionRemovesTheCallThatOnlyPicked,TestAShrugChoosesNothing,TestAnythingGoingWrongJustChoosesNothing).TestEveryHopCarriesTheAgentAndTheCallersRequestIsUntouched), and the caller's deadline still applies (TestTheCallersDeadlineStillApplies).How to test
go test ./.... Every package reportsok.go build -ldflags "-X github.com/mindsdb/yolocoder/internal/version.Version=main" -o /tmp/yolocoder ./cmd/yolocoder. The agent string uses only the version, and a build with no stamped commit never self-updates, so a re-run cannot swap the binary for the published one./tmp/ua.log, returns a Cloudflare-style 403 on/v1/decisions, and returns a finished reply everywhere else.mkdir -p /tmp/yh/.config/yolocoder /tmp/yw && echo 'const x = 1;' > /tmp/yw/a.ts, then write{"version":1,"provider":"openai-compatible","base_url":"http://127.0.0.1:18765","model":"m","api":"responses","preselect":true}to/tmp/yh/.config/yolocoder/config.jsonand{"api_key":"not-a-real-key"}tocredentials.jsonbeside it.cd /tmp/yw && HOME=/tmp/yh YOLOCODER_NO_AUTOUPDATE=1 /tmp/yolocoder "look at x". The trail showsfile preselection skipped: decisions returned 403 Forbidden, thenfile choice, then the replyNothing to change./tmp/ua.log. BothPOST /v1/decisionsandPOST /v1/responsesshowUA=yolocoder/main (+https://github.com/mindsdb/yolocoder).mainis green, start yolocoder once so it self-updates, turn on/preselect, and run a task against MindsHub. In Cloudflare Security Events or the HTTP request logs forapi.mindshub.ai, the/v1/decisionsrequest from your IP showsyolocoder/main (+https://github.com/mindsdb/yolocoder)and noGo-http-client/2.0. This is the check most likely to expose a partial fix: any call path that still sends Go's default shows up here.Local server for steps 3 to 6
Notes for the reviewer
internal/httpclient.Clientwrapshttp.DefaultTransportand setsUser-Agenton a clone of every request, redirects included. Every call site uses it. The edit router's per-callYoloCoder/experimental-edit-routerheader is gone because the transport would overwrite it anyway. The transport sets the header unconditionally rather than only when empty, since no caller has a reason to send anything else.auth.mindshub.aiandgithub.com, notapi.mindshub.ai. Putting them on the same client keeps the guard's exempt list to two files: the client itself, and the--webpreview proxy ininternal/web/proxy.go, which forwards the browser's own requests to the local dev server.CONNECTthat opens a tunnel through anHTTPS_PROXYcarriesGo-http-client/1.1, becausenet/httpwrites it below anyRoundTripper. The request inside the tunnel carries the yolocoder agent, so the edge never sees Go's default. The--webpreview proxy forwards the browser's agent tolocalhost. The README says both.mainon every rolling build. The Release workflow stampsmainon pushes and the tag name onv*tags. I left the commit out of the header:version.Display()rendersmain (abc1234), which is not a valid product token, and the edge only needs to tell yolocoder apart from Go's default.file preselection skipped:line. No turn can be cancelled today, because every turn runs oncontext.Background()(cmd/yolocoder/main.go,internal/web/server.go), so those errors are always the call's own. A call whose turn context has already ended still prints nothing, so a cancel added later stays quiet. A 2xx that fails to read or parse stays in theYOLOCODER_DEBUG_LOGtrace. The status comes from the code and Go's name for it, not the server's reason phrase./v1/decisionsand/preselecton now printsdecisions returned 404 Not Foundevery turn. That line is accurate. Typing/preselectin a terminal session turns it off. The--webwindow does not take commands, and a running--webserver keeps the setting it started with, so it picks up the change when it restarts.yolocoder/main (+https://github.com/mindsdb/yolocoder)from this public repo with one line of code. Treat the prefix block as a speed bump, not as proof that a request is ours. Per-key and per-IP limits are the durable signal.maincuts the release..github/workflows/release.ymlrebuilds the six binaries withVersion=mainand republishes the rollinglatestGitHub release. Installed release builds checklateston every launch (throttleChecks = falseininternal/update/update.go) and replace themselves, so most users get this the next time they start yolocoder. Av*tag would make a permanent release, but none exist and the self-updater never reads them. The update check goes to GitHub, so a block onapi.mindshub.aicannot stop the upgrade itself.--web) until they restart, anyone withYOLOCODER_NO_AUTOUPDATE=1, and source builds with no stamped commit (go run, plaingo build), which never self-update. For them,/preselectstops picking files and the turn runs as if it were off, with nothing on screen./preselectis off by default, so only users who turned it on see a difference. Coding calls are untouched while the rule stays scoped to/v1/decisions.selectEditFilesbuilds its payload and questions asmap[string]any, whiledecide.gouses typeddecisionRequestandquestionstructs. It also builds its decisions URL inline instead of callingdecisionsEndpoint. Both predate this PR and stay out of scope.http.DefaultClient, so it also sends Go's default agent. It callsGET /v1/modelsonapi.mindshub.ai(internal/catalog/catalog.go, internal/adapters/modelprobe.go) and the usage and sign-in routes onauth.mindshub.ai(internal/usage/usage.go, internal/auth/auth.go). It never calls/v1/decisions, so aGo-http-client/rule scoped to that path leaves it alone. A rule that widens past/v1/decisionswould break it, and this PR does nothing for it.internal/web/template/scratch/backend/decisions.tsruns inside the user's generated app, so it sends Node's agent, and aGo-http-client/rule does not touch it.Verified locally
go build,go vet,go test ./...,gofmt -l .onorigin/mainbefore the changeokgo build,go vet,go test ./...,gofmt -l .on this branchokgo test -raceon the new and changed testsokGOOS=windows go vet ./...okhttp.DefaultClient(NewClient, chooser, edit router, coding call,ListModels,DetectAPI,CreateAPIKey,exchangeCode, updater default)User-Agent = "Go-http-client/1.1", andTestNoCodeBypassesTheSharedClientfails for eachGo-http-client/2.0TestEveryHopCarriesTheAgentAndTheCallersRequestIsUntouchedfailsTestAnythingGoingWrongJustChoosesNothingfailsVersion=main) against the local 403 serveryolocoder/main (+https://github.com/mindsdb/yolocoder)on/v1/decisionsand/v1/responses; trail showsfile preselection skipped: decisions returned 403 Forbidden; the turn finishesorigin/mainbinary against the same serverGo-http-client/1.1on both routes; no trail lineGET https://api.mindshub.ai/cdn-cgi/traceandhttps://auth.mindshub.ai/cdn-cgi/tracethroughhttpclient.ClientwithVersion=main. The Cloudflare edge answers this path itself and never forwards it to the originuag=yolocoder/main (+https://github.com/mindsdb/yolocoder)andhttp=http/2, so the edge sees the new agent over HTTP/2http.DefaultClientuag=Go-http-client/2.0, the agent the widened rule would matchClone,http.DefaultClientback inNewClient,ListModels,DetectAPI,CreateAPIKey,exchangeCodeand the updater default, no trail line on a non-2xx, a trail line on a 200 that fails to parse), each restored from a snapshot and checked withcmpTestNoCodeBypassesTheSharedClientua.logmatch the stepsHEAD https://api.mindshub.ai/v1/decisionsGo-http-client/2.0gets 403 from the #231 rule;yolocoder/main (+https://github.com/mindsdb/yolocoder)gets 401, past the edgego vet,GOOS=windows go vet ./...,go test ./...,gofmt -l ., andgo test -raceon agent, httpclient, auth and updateokClone(context.Background())in the transport; the guard forgettingDefaultTransport; the guard allowing value clients)new(http.Client), a zero-valuehttp.Client,http.DefaultTransport.RoundTrip, an aliased and a dot import, and&http.Transport{}httpfile choice, thenNothing to change.;ua.logshows the yolocoder agent on/v1/decisionsand/v1/responsesapi.mindshub.ai/v1/decisionscallShips with
Go-http-client/1.1agent onapi.mindshub.ai/v1/decisions. #231 widened that rule in place tostarts_with(http.user_agent, "Go-http-client/"), merged on 2026-09-27 at 11:17 UTC, and is live. Its rule comment and its test already describe the agents this PR sends, so no terraform follow-up is needed./preselecton loses preselection on every turn. Merging cuts the release, and each user recovers at their next launch. There is no deploy coupling, and yolocoder has no per-PR environment.