Skip to content

Commit a1c4151

Browse files
authored
Get Bots past bot checks: headed browsers, Web Bot Auth, a residential proxy option, Bun 1.4.3, and a handoff that keeps its page (CopilotKit#772)
* Keep the challenge page through a computer restart Opening a Cloudflare-fronted page could restart the computer, and the handoff that followed showed the person about:blank with nothing to solve. The restart: the egress filter pinned each connection to its checked addresses with autoSelectFamily, and Bun's multi-address connect can throw from a timer after the attempt is gone (oven-sh/bun#25633). Nothing catches that throw, so the process exited. Cloudflare hosts resolve to several addresses and a challenge page opens and drops many connections, which is when it fired. The filter now tries one checked address at a time, IPv4 first, with its own per-attempt timeout, and the process turns multi-address connect off for every other connect. The blank page: a restarted or relaunched browser comes back on about:blank and nothing sent it anywhere. A handoff request now records the page it was raised on, and a request made from a blank browser shortly after a restart inherits the interrupted request's page. When a request is made or taken and the browser is blank, the computer opens that page again through the normal navigation path, so egress and page-access rules still apply. Spent __cf_chl_* challenge tokens are not kept. * Run on Bun 1.4.3, where the multi-address connect crash is fixed Bun 1.4.0 fixed the autoSelectFamily timer that threw 'null is not an object (evaluating context)' and exited the computer (oven-sh/bun#25633, fixed by oven-sh/bun#32660). Bun's own regression fixture fails on 1.3.14 and passes on 1.4.3. Every pin moves: the root packageManager, CI and release workflows, the computer, server, supervisor and Bot images (computer images by digest), the desktop runtime and its checksums, and prompt.txt. The single-address egress connect stays as defence in depth. * Run hosted computers' browsers headed by default Headless Chromium is what bot checks catch first: it says HeadlessChrome and Cloudflare challenges it on sight, even from a residential address. Every hosted computer runs on Linux (a container, a pod, the desktop app's engine) and already ships Xvfb, so the managed browser now defaults to headed there, drawing to the computer's own display behind the same live screen. Compose defaults the shared computer and supervised ones to headed too. A computer run directly on macOS or Windows stays headless, where headed would open windows on the desktop, and COMPUTER_BROWSER_MODE=headless still chooses headless anywhere. * Sign every request a Bot's browser sends with Web Bot Auth Cloudflare lets a signed agent through where it would challenge an unknown browser, which is how OpenAI's agent gets past it. The server now holds one Ed25519 key, derived from KEY_ENCRYPTION_KEY or given as WEB_BOT_AUTH_KEY, and publishes its public half, signed over ("@authority";req), at /.well-known/http-message-signatures-directory. The key reaches each computer inside the network-policy push the server already sends. The computer signs in one browser-level DevTools Fetch session, so every request is covered whatever sent it: pages, redirects, cross-site frames in their own processes, dedicated workers, service workers and their script fetches. Headers are added as the request leaves the browser, so cookies, CORS, TLS and the egress filter are unchanged. Signature-Agent is the bare string Cloudflare documents, with @authority and signature-agent covered, a 60 second expiry and a nonce. WEB_BOT_AUTH unset means on when the public URL is https on a public address, which a verifier can fetch the key from, and off otherwise. A headless browser also stops calling itself HeadlessChrome, on the wire and to scripts. * Chart settings for headed browsers, Web Bot Auth and a residential proxy computers.browserMode (headed by default) and computers.egressProxy reach the shared computer and every per-Bot sandbox through one helper, so the two modes cannot drift. The residential or ISP proxy URL is a secret (secrets.egressProxyUrl or a Secret of your own) and arrives as EGRESS_PROXY_DEFAULT behind the egress filter. It is off by default, and the chart refuses it with network policies on and no path to the proxy, or switched on with no URL. config.webBotAuth and secrets.webBotAuthKey pass Web Bot Auth through. Every new key is guarded for --reuse-values upgrades. The proxy is tested end to end against a gateway that demands Basic credentials, for plain HTTP and CONNECT, including that the policy is decided before the gateway is asked. * Document getting past bot checks How a Bot's browser presents itself and what each setting changes: headed Chromium, Web Bot Auth with the one-time Cloudflare registration, and a residential proxy as the operator's choice. The handoff is described as the designed fallback for a check a Bot cannot pass. * Get the Bun 1.4 and headed-browser CI runs passing The tests job never installed agent-computer's own dependencies, so the Web Bot Auth tests could not import web-bot-auth under the root run. It now installs them, as it already does for the other packages that keep their own lockfile. Bun 1.4 reports a refused fetch as a TypeError with code ECONNREFUSED instead of an Error with code ConnectionRefused, so the tool-selection driver's stopped-listener check matched neither shape. It now matches on the error code from either version. The headed-browser test navigated to a data: URL, which /navigate has refused since it was hardened; it had never run in CI until this branch asked for it. It now serves the page from a local http server. * Keep a handoff's page only across a restart, and reopen it in the background A stop or reset left the request interrupted exactly like a process restart, so a help request from a blank browser within ten minutes reopened the page the reset was meant to forget. Only a restart underneath the request (the process or the browser coming back up) now marks it as keeping its page; the stop and reset routes clear it, including after the browser close that reaches renewRun first. A finished request no longer keeps its page. The address can carry a one-time token and the record stays on disk for a month, so finish() drops it for every outcome but a restart, a restart's page is dropped once the help TTL passes, and records written before this are cleaned when they are read. The help call and Take control no longer wait for the page to reopen, which could take a cold browser start plus a full navigation timeout. The restore runs in the background, one at a time per Bot, and gives up if a person has sent input or the request has ended by the time the browser is ready.
1 parent c99a1a5 commit a1c4151

62 files changed

Lines changed: 3403 additions & 121 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,25 @@ COMPUTER_TOKEN=
329329
# docker-compose.yml. Pointing it somewhere unmounted disables login persistence.
330330
# PROFILES_DIR=/profiles
331331

332-
# Browser process used by a Bot's computer. Managed mode always uses full Chromium. `headless` is
333-
# the default; Linux `headed` uses a private virtual display with the same live-screen controls.
332+
# Browser process used by a Bot's computer. Managed mode always uses full Chromium. `headed` is the
333+
# default wherever a hosted computer runs (Linux: a container, a pod, the desktop app's engine): full
334+
# Chromium on the computer's own virtual display, behind the same live screen, which bot checks such
335+
# as Cloudflare's challenge far less than headless. `headless` is still available, smaller, and the
336+
# default only when the computer runs directly on macOS or Windows, where headed would open windows.
334337
# COMPUTER_BROWSER_BACKEND=managed
335-
# COMPUTER_BROWSER_MODE=headless
338+
# COMPUTER_BROWSER_MODE=headed
339+
340+
# Web Bot Auth: every request a Bot's browser makes is signed as coming from this deployment
341+
# (Ed25519, HTTP Message Signatures), and the public key is published at
342+
# <OPENBOT_PUBLIC_URL>/.well-known/http-message-signatures-directory. Unset means on whenever
343+
# OPENBOT_PUBLIC_URL (or BETTER_AUTH_URL) is https on a public address, because verifiers fetch the
344+
# key from there; off otherwise, which includes every laptop. `on` also signs behind a private https
345+
# address, and is refused for plain http or loopback. It only earns anything once the
346+
# deployment is registered with Cloudflare, a manual step: see docs/configuration.md.
347+
# WEB_BOT_AUTH=on
348+
# The signing key, an Ed25519 private key as a JSON Web Key. Unset derives one from
349+
# KEY_ENCRYPTION_KEY, the same on every replica, so nothing new is needed to start. Never logged.
350+
# WEB_BOT_AUTH_KEY=
336351

337352
# Opt-in installed Chrome on the same machine as a local API deployment:
338353
# bun scripts/start-local-chrome-computer.ts
@@ -364,6 +379,12 @@ COMPUTER_TOKEN=
364379
#
365380
# EGRESS_PROXY_DEFAULT=http://user:password@proxy.internal:8080
366381
# EGRESS_PROXY_SALES_BOT=http://sales.proxy.internal:8080
382+
#
383+
# A residential or ISP proxy goes in the same place, and is the operator's choice to make knowingly:
384+
# the Bots' traffic then leaves from an address a consumer ISP owns, so sites see what looks like a
385+
# person at home rather than a datacenter. Off unless set. The egress filter still applies first.
386+
#
387+
# EGRESS_PROXY_DEFAULT=http://customer-user:password@gateway.residential.example:8000
367388

368389

369390
# The managed coworker AG-UI endpoint. Optional: use an HTTP(S) URL, and set MANAGED_AGENT_TOKEN

‎.github/workflows/ci.yml‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
persist-credentials: false
3131
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
3232
with:
33-
bun-version: 1.3.14
33+
bun-version: 1.4.3
3434
- run: bun install --frozen-lockfile
3535
- run: bun run format:check
3636
- run: bun run lint
@@ -75,7 +75,7 @@ jobs:
7575
persist-credentials: false
7676
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
7777
with:
78-
bun-version: 1.3.14
78+
bun-version: 1.4.3
7979
# Two installs, and the root one is load-bearing. Both packages set `types: ["bun"]` in
8080
# their tsconfig while `@types/bun` is a root devDependency, so tsc resolves it by walking up
8181
# to the root node_modules. Without it the typecheck fails with TS2688 on a fresh checkout.
@@ -118,19 +118,34 @@ jobs:
118118
persist-credentials: false
119119
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
120120
with:
121-
bun-version: 1.3.14
121+
bun-version: 1.4.3
122122
- run: bun install --frozen-lockfile
123123
- run: bun install --frozen-lockfile
124124
working-directory: agent-computer
125125
- run: bunx playwright install --with-deps chromium
126126
working-directory: agent-computer
127+
# Headless here: these launch through the profile path without the computer's own X server.
128+
# The headless run of the Web Bot Auth test is also the one that checks the user agent fix.
127129
- run: >-
128130
bun test tests/sign-in.test.ts tests/session-cookies.test.ts tests/follows-popup.test.ts
129-
tests/demonstration-live.test.ts tests/webrtc-proxy.test.ts
131+
tests/demonstration-live.test.ts tests/webrtc-proxy.test.ts tests/web-bot-auth-browser.test.ts
132+
tests/handoff-page-restore.test.ts
130133
working-directory: agent-computer
131134
env:
132135
OPENBOT_COMPUTER_BROWSER: '1'
133136
OPENBOT_DEMONSTRATION_LIVE: '1'
137+
COMPUTER_BROWSER_MODE: headless
138+
# Headed is the default for hosted computers, so it is tested as one: the computer starting its
139+
# own Xvfb, and the signing again with cross-site frames in processes of their own.
140+
- run: bun test tests/headed-browser.test.ts
141+
working-directory: agent-computer
142+
env:
143+
OPENBOT_HEADED_BROWSER: '1'
144+
- run: xvfb-run --auto-servernum bun test tests/web-bot-auth-browser.test.ts
145+
working-directory: agent-computer
146+
env:
147+
OPENBOT_COMPUTER_BROWSER: '1'
148+
COMPUTER_BROWSER_MODE: headed
134149

135150
chart:
136151
name: chart (${{ matrix.target }})
@@ -153,7 +168,7 @@ jobs:
153168
# For the coherence check below, which is a Bun script like everything else here.
154169
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
155170
with:
156-
bun-version: 1.3.14
171+
bun-version: 1.4.3
157172
# Nothing rendered this chart until now, which is how four values files that produce a server
158173
# unable to start were shipped and stayed shipped. Rendering is the cheap half; the refusals in
159174
# validation.yaml are the half that catches a missing value before a cluster does.
@@ -218,6 +233,19 @@ jobs:
218233
--set networkPolicy.enabled=true \
219234
--set computers.extraEnv[0].name=EGRESS_PROXY_DEFAULT \
220235
--set-string computers.extraEnv[0].value=http://proxy.internal:3128
236+
# The residential proxy setting, whose address is a secret the template cannot read, with
237+
# network policies on and no path opened to it.
238+
refuses "a residential proxy with no path through the network policy" \
239+
--set networkPolicy.enabled=true --set computers.egressProxy.enabled=true \
240+
--set-string secrets.egressProxyUrl=http://user:pass@gateway.example:8000
241+
# The same setting switched on with no URL behind it, where this chart holds the secret.
242+
if ! grep -qE '^ *enabled: true' <(sed -n '/^externalSecrets:/,/^[a-z]/p' charts/openbot/ci/${{ matrix.target }}-values.yaml) \
243+
&& ! grep -qE '^ *existingSecret: [^"]' <(sed -n '/^secrets:/,/^[a-z]/p' charts/openbot/ci/${{ matrix.target }}-values.yaml); then
244+
refuses "a residential proxy switched on with no URL" \
245+
--set computers.egressProxy.enabled=true
246+
else
247+
echo "skipped: the proxy URL refusal is armed only where this chart writes the Secret"
248+
fi
221249
# A delivery or directory switch with no secret behind it. Armed on every target: the chart
222250
# requires the value when it holds the secret, and requires the store to name the key when
223251
# a store does.
@@ -282,7 +310,7 @@ jobs:
282310
persist-credentials: false
283311
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
284312
with:
285-
bun-version: 1.3.14
313+
bun-version: 1.4.3
286314
- run: bun install --frozen-lockfile
287315
# The Bots are not root workspaces and each keeps its own lockfile. The startup tests spawn
288316
# the real entrypoint, so its dependencies have to be installed as well.
@@ -292,6 +320,10 @@ jobs:
292320
# Bot's own tree and not in the root one.
293321
- run: bun install --frozen-lockfile
294322
working-directory: agent-langgraph
323+
# Same for the computer: its Web Bot Auth tests import `web-bot-auth`, which lives in
324+
# agent-computer's own tree and not in the root one.
325+
- run: bun install --frozen-lockfile
326+
working-directory: agent-computer
295327
# Same for the Mastra Bot: root test discovery imports its receiver tests, which load Mastra
296328
# from that Bot's own dependency tree.
297329
- run: bun install
@@ -321,7 +353,7 @@ jobs:
321353
persist-credentials: false
322354
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
323355
with:
324-
bun-version: 1.3.14
356+
bun-version: 1.4.3
325357
- run: bun install --frozen-lockfile
326358
- run: bun test app/tests/build-cache.test.ts
327359

@@ -337,7 +369,7 @@ jobs:
337369
python-version: "3.12"
338370
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
339371
with:
340-
bun-version: 1.3.14
372+
bun-version: 1.4.3
341373
- name: CrewAI provider-boundary regression
342374
run: |
343375
set -euo pipefail
@@ -430,7 +462,7 @@ jobs:
430462
persist-credentials: false
431463
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
432464
with:
433-
bun-version: 1.3.14
465+
bun-version: 1.4.3
434466
- run: bun install --frozen-lockfile
435467
- run: bun run build
436468

@@ -449,7 +481,7 @@ jobs:
449481
persist-credentials: false
450482
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
451483
with:
452-
bun-version: 1.3.14
484+
bun-version: 1.4.3
453485
- run: bun install --frozen-lockfile
454486
# Collisions and gaps between the migration files themselves.
455487
- run: bunx drizzle-kit check --config=drizzle.config.ts

‎.github/workflows/desktop-signing.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989
if ($LASTEXITCODE -ne 0) { throw 'Could not read signing certificate.' }
9090
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
9191
with:
92-
bun-version: 1.3.14
92+
bun-version: 1.4.3
9393
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
9494
with:
9595
toolchain: stable

‎.github/workflows/desktop.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
persist-credentials: false
7171
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
7272
with:
73-
bun-version: 1.3.14
73+
bun-version: 1.4.3
7474
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
7575
with:
7676
toolchain: stable

‎.github/workflows/publish-release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
persist-credentials: false
8787
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
8888
with:
89-
bun-version: 1.3.14
89+
bun-version: 1.4.3
9090
- env:
9191
VERSION: ${{ needs.metadata.outputs.version }}
9292
run: |

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
persist-credentials: false
5454
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
5555
with:
56-
bun-version: 1.3.14
56+
bun-version: 1.4.3
5757

5858
- id: prepare
5959
name: Write the version and promote the notes

‎CHANGELOG.md‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,40 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
**Before upgrading.** Four things change for an existing deployment:
12+
- A hosted computer's browser is now headed by default. It uses a little more memory than headless
13+
did; set `COMPUTER_BROWSER_MODE=headless` (Helm: `computers.browserMode: headless`) to keep the old
14+
behaviour.
15+
- A deployment whose public URL is https on a public address starts signing its Bots' requests with
16+
Web Bot Auth; `WEB_BOT_AUTH=off` (Helm: `config.webBotAuth: "off"`) prevents it.
17+
- Migration `0054_memory_on_intelligence` adds two columns to `personal_memories`. On a deployment
18+
entitled to Intelligence memory, the memory sweep then copies existing reviewed, switched-on memories
19+
into Intelligence, a few people at a time each minute; it is idempotent and safe to interrupt.
20+
Memories awaiting review, paused or forgotten stay local until that changes.
21+
- With nothing saved under Admin → Automatic Learning and no `CPK_INTELLIGENCE_LEARNING_CONTAINER_ID`,
22+
the server assigns its Intelligence project's `openbot` container once it exists, and `scripts/start.sh`
23+
creates that container. A saved setting, including off, is never changed;
24+
`OPENBOT_LEARNING_AUTO_ASSIGN=false` (Helm: `config.learning.autoAssign: false`) prevents it.
25+
26+
### Fewer "Verify you are human" pages
27+
28+
Sites behind Cloudflare challenged a Bot's browser on sight, and opening one could restart the
29+
computer and leave the person a blank page to solve. Now:
30+
- The computer no longer restarts on such a page, and a help request reopens the page the Bot was
31+
on, so the person taking control always gets the check to solve. That handoff is the designed
32+
fallback for a check a Bot cannot pass.
33+
- A hosted computer runs headed Chromium on its own virtual display, behind the same live screen.
34+
Headless Chromium is what bot checks catch first. A computer still run headless no longer calls
35+
itself `HeadlessChrome`.
36+
- With a public https URL, every request a Bot's browser sends is signed with Web Bot Auth and the
37+
key is published at `/.well-known/http-message-signatures-directory`. Register the deployment once
38+
with Cloudflare (Bot Submission Form, Request Signature) for sites that allow signed agents to let
39+
Bots through. See docs/configuration.md. Every site a Bot visits sees the deployment's URL.
40+
- A residential or ISP proxy is a first-class setting: `EGRESS_PROXY_DEFAULT`, or on Helm
41+
`computers.egressProxy` with the URL in a Secret. Off by default; turning it on makes Bots' traffic
42+
look like a person's home connection, which is the operator's choice to make.
43+
- Bun 1.4.3 throughout, which fixes the connection crash behind the restarts.
44+
1145
### Automatic Learning assigns its container without a trip to Admin
1246

1347
Learning was on by default but collected nothing until somebody assigned a container. Now, when
@@ -29,11 +63,6 @@ and which Bot or source it came from, and applies those rules to everything Inte
2963
Bot-formed memory still reaches only the Bot that formed it. Without Intelligence memory nothing changes:
3064
OpenBot's own tables remain the store.
3165

32-
**Before upgrading.** Migration `0054_memory_on_intelligence` adds two columns to `personal_memories`.
33-
On an entitled deployment, the memory sweep then copies existing reviewed, switched-on memories into
34-
Intelligence, a few people at a time each minute; it is idempotent and safe to interrupt. Memories
35-
awaiting review, paused or forgotten stay local until that changes.
36-
3766
## 0.2.0
3867

3968
**Before upgrading.** Two things change for an existing deployment:

‎Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# image. Keep this version matched to `agent-computer/package.json`: bump both or neither.
1919

2020
FROM node:24.18.1-bookworm-slim AS node-toolchain
21-
FROM oven/bun:1.3.14@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS bun-toolchain
21+
FROM oven/bun:1.4.3@sha256:ec06c3b6cea04192ae6770c434f668ca41d343ad19fa6472216c7b48be39c598 AS bun-toolchain
2222

2323
FROM ubuntu:24.04 AS base
2424

‎agent-bot/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The built-in Bot only speaks AG-UI and a model provider; the browser runs in a separate computer
22
# container so either side can be replaced independently.
3-
FROM oven/bun:1.3-alpine
3+
FROM oven/bun:1.4-alpine
44

55
WORKDIR /app
66
RUN chown bun:bun /app

‎agent-computer/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# The image tag and Playwright dependency must be pinned to the same exact version. Bump both or
44
# neither.
55
FROM node:24.18.1-bookworm-slim AS node-toolchain
6-
FROM oven/bun:1.3.14@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS bun-toolchain
6+
FROM oven/bun:1.4.3@sha256:ec06c3b6cea04192ae6770c434f668ca41d343ad19fa6472216c7b48be39c598 AS bun-toolchain
77

88
FROM ubuntu:24.04
99

0 commit comments

Comments
 (0)