From be7c6451168728f531dcd02533b385e889a1dd80 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Wed, 4 Nov 2026 22:01:19 -0500 Subject: [PATCH] Keep a Cloudflare challenge's long address within the saved limit 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. --- CHANGELOG.md | 4 ++++ agent-computer/src/control.ts | 12 +++++++++--- .../tests/control-handoff-page.test.ts | 11 ++++++++++- agent-computer/tests/control-store.test.ts | 17 +++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00459f6ea7..47441918cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,10 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. its tools to every Bot, including Bots made later. It can no longer be removed, and a Bot's Routines tools can no longer be taken away. +### A Cloudflare challenge on a long address no longer stops a Bot's computer + +When a Bot met a Cloudflare challenge, the computer kept the page's address without the challenge's one-time `__cf_chl_*` tokens. Removing them rewrote the rest of the query, so characters such as `:` and `/` came back as `%3A` and `%2F`, and an address that fit the 2,048-character limit could grow past it. The computer then could not save the request, and from then on refused every action from that Bot with "The handoff state could not be saved". The tokens are now removed from the query as it was written, and an address still too long is not kept. + ### A personal edition for one person on one computer Set `OPENBOT_EDITION=personal`, with single-user mode and one computer, for an OpenBot without the diff --git a/agent-computer/src/control.ts b/agent-computer/src/control.ts index eb7ecd1b31..d630e3891f 100644 --- a/agent-computer/src/control.ts +++ b/agent-computer/src/control.ts @@ -67,9 +67,15 @@ export function pageWorthKeeping(url: unknown): string | undefined { const parsed = new URL(url); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined; - for (const name of [...parsed.searchParams.keys()]) - if (name.startsWith("__cf_chl_")) parsed.searchParams.delete(name); - return parsed.toString(); + // Filter the query as written: rewriting it through `searchParams` re-encodes every value, so a + // kept address could outgrow MAX_REQUEST_URL, which the store refuses to save. + const kept = parsed.search + .slice(1) + .split("&") + .filter((pair) => !pair.startsWith("__cf_chl_")); + parsed.search = kept.join("&"); + const page = parsed.toString(); + return page.length > MAX_REQUEST_URL ? undefined : page; } catch { return undefined; } diff --git a/agent-computer/tests/control-handoff-page.test.ts b/agent-computer/tests/control-handoff-page.test.ts index 23fe4dc955..df5f4b88ba 100644 --- a/agent-computer/tests/control-handoff-page.test.ts +++ b/agent-computer/tests/control-handoff-page.test.ts @@ -6,7 +6,7 @@ import { HELP_REQUEST_TTL_MS, pageWorthKeeping, } from "../src/control"; -import type { StoredControl } from "../src/control-store"; +import { MAX_REQUEST_URL, type StoredControl } from "../src/control-store"; import { createSessions } from "../src/sessions"; /** A store that survives "restarts": a new control built on it is the computer coming back up. */ @@ -285,6 +285,15 @@ describe("only a real page is worth keeping", () => { ), ).toBe("https://chatgpt.com/share/abc?ref=mail"); }); + + test("dropping a challenge token leaves the rest of the query as it was written", () => { + const next = `https://shop.example.com/${"a/".repeat(500)}`; + const url = `https://shop.example.com/login?next=${next}&flag&__cf_chl_rt_tk=abc`; + expect(url.length).toBeLessThan(MAX_REQUEST_URL); + expect(pageWorthKeeping(url)).toBe( + `https://shop.example.com/login?next=${next}&flag`, + ); + }); }); describe("a detected challenge remembers its page", () => { diff --git a/agent-computer/tests/control-store.test.ts b/agent-computer/tests/control-store.test.ts index 203052b23a..9ad0a16e36 100644 --- a/agent-computer/tests/control-store.test.ts +++ b/agent-computer/tests/control-store.test.ts @@ -147,3 +147,20 @@ test("who holds the wheel is saved with it, a state saved before it loads, and a writeFileSync(path, JSON.stringify({ ...saved, heldBy: "user-1" })); expect(() => createControl(undefined, { store })).toThrow(); }); + +test("a challenge on a long address is recorded, and later requests still save", () => { + const { control } = fixture(); + const next = `https://shop.example.com/${"a/".repeat(500)}`; + const url = `https://shop.example.com/login?next=${next}&__cf_chl_rt_tk=abc`; + const first = control.requestHelp( + "Complete the challenge.", + "call-1", + "cloudflare", + url, + ).request!; + expect(first.url).toBe(`https://shop.example.com/login?next=${next}`); + expect( + control.requestHelp("Sign in", "call-2", "model", "https://example.com/") + .request, + ).toBeDefined(); +});