Repository navigation
Keep a Cloudflare challenge's long address within the saved limit - #1013
Open
rudycelekli wants to merge 1 commit into
Open
rudycelekli wants to merge 1 commit into
rudycelekli wants to merge 1 commit into
Conversation
pageWorthKeeping dropped __cf_chl_* parameters through searchParams, which re-encodes the whole query. An address under MAX_REQUEST_URL could come back longer than it, the control store refused to save the request, and the Bot's computer closed action admission. Filter the query as written instead, and check the length of what is kept.
rudycelekli
requested review from
MikeRyanDev,
davidmckayv,
dustywusty,
guidovizoso,
mxmzb,
samjulien and
tylerslaton
as code owners
November 5, 2026 03:11
This branch has not been deployed
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 this changes
A Cloudflare challenge on a long address can switch off a Bot's computer.
When the computer sees a challenge,
detectChallenge(agent-computer/src/challenge.ts) raises a handoff withpage.url().pageWorthKeeping(agent-computer/src/control.ts) then decides what address to keep. It checks the length of the address it was given and drops the__cf_chl_*parameters withparsed.searchParams.delete(). Deleting throughsearchParamsre-serialises the whole query with form encoding, so every:,/,,,@and;in the other parameters comes back as%XX. An address underMAX_REQUEST_URL(2,048) can come back longer than it.The control store (
control-store.ts,validRequest) refuses a request whoseurlis over 2,048 characters, sosavethrows "Corrupt computer control state.".persistturns that into the sticky "The handoff state could not be saved. Computer actions are disabled until storage is repaired." and closes the action barrier. Every laterpersistfor that Bot throws it again.Repro (the same case as the new test):
https://shop.example.com/login?next=https://shop.example.com/a/a/…(500 × "a/")&__cf_chl_rt_tk=abcis 1,083 characters.pageWorthKeepingreturns 2,069 characters, becausenext=https://shop.example.com/a/a/…becomesnext=https%3A%2F%2Fshop.example.com%2Fa%2Fa%2F….requestHelpthen throws. A second, ordinaryrequestHelp(..., "https://example.com/")throws the same error.How a deployment reaches it: Cloudflare adds
__cf_chl_rt_tkto the address of the page it challenges and keeps the original query. A sign-in or checkout page with a return address in its query is enough. The store is on by default (PROFILES_DIR, default/profiles). The same rewrite also turns?flaginto?flag=.The fix drops the
__cf_chl_*pairs from the query as it was written and puts what is left back on the URL, so nothing else is re-encoded. The length check now applies to the address that is kept. One that is still too long is not kept, as already happens when the input itself is too long.Where it runs
OpenBot is deployed as several server processes behind a load balancer, serving a whole company.
Consecutive requests from the same person reach different processes, and the process that answered a
WebSocket upgrade is rarely the one that answers the next call on that conversation.
State that outlives a single request therefore has to be shared, or the change works on one machine
and stops working the moment there are two, without saying so. That failure is worse than not
shipping the feature: it passes review, passes CI, passes a local demo, and only surfaces as a Bot
that forgets, a question nobody can answer, or a boundary that never fires.
Answer these even when the answer is "none":
pageWorthKeepingis a pure function. The request it feeds is saved in the existing per-Bot control file.Postgres is already there and is the default answer to all of the above: a table, a unique index, a
conditional update,
LISTEN/NOTIFYfor fan-out.Boundary and audit
Changelog
CHANGELOG.mdunderUnreleased:Proof
New tests:
agent-computer/tests/control-handoff-page.test.ts,dropping a challenge token leaves the rest of the query as it was written: an address withnext=https://…, a bareflagand a__cf_chl_rt_tkkeeps everything except the token, byte for byte.agent-computer/tests/control-store.test.ts,a challenge on a long address is recorded, and later requests still save: with the real file store, the challenge request is saved with the cleaned address, and the next request saves too.Before the fix (
agent-computer/src/control.tsreverted toorigin/main, tests kept):After the fix:
The existing
a spent Cloudflare challenge token is not kept, the rest of the address iscases pass unchanged.Gates:
biome formatandbiome lint --error-on-warnings(2.5.10) report nothing on the three touched files.tsc --noEmit -p agent-computerreports no errors in the touched files. It ran against symlinked dependencies, and the only error it reports is that it cannot resolvespiffe(src/identity.ts, a file this change doesn't touch).