Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions agent-computer/src/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
11 changes: 10 additions & 1 deletion agent-computer/tests/control-handoff-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand 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", () => {
Expand Down
17 changes: 17 additions & 0 deletions agent-computer/tests/control-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});