Skip to content

Commit 24bc66b

Browse files
authored
The daemon runs its own browser for the bridge (#1332) (#1718)
* The daemon runs its own browser for the bridge (#1332) Option A from the spike, on top of the Driver tab: a preference makes the daemon launch a Chrome for Testing of its own — downloaded once into ~/.the-framework-browser, a persistent profile, the extension installed over CDP (Extensions.loadUnpacked, which also grants its site access), developer mode switched on so #1712's self-reload keeps working, the daemon URL and bridge token seeded into the extension's storage, the pinned Driver tab opened on claude.ai's session list, and the window minimized. Headed because Cloudflare rejects headless; minimized because macOS clamps any off-screen position back onto the screen, and minimized passes. Settings gets the switch and a line: the launch step (download percentage included), running with Show/Hide for the one-time sign-in — the daemon reads its own tab's URL to say a sign-in is needed, since the bridge's last hello may be the user's own Chrome's — and stopped with the reason plus Restart. A browser a dead daemon left holding the profile is stopped first; the daemon's shutdown closes the browser. With two Drivers possible (the user's Chrome and this browser), /_bridge/answer now claims the answer on read for 90 s, so a session served by both is never answered twice. * The Driver overlay stands aside on claude.ai's sign-in page (#1332) The overlay covered the sign-in page too, so the one thing a person has to do in the daemon's bridge browser — sign in, once — was impossible. On /login and /logout the overlay is taken down and not drawn; it returns with the next page. Harness case added. * The bridge browser keeps its cookie key out of the OS keychain (#1332) macOS asked for the login password on every launch of Chrome for Testing ("Chromium Safe Storage"), which an unattended browser can never answer. --use-mock-keychain and --password-store=basic, as Puppeteer launches: the profile directory's permissions guard the sign-in instead. * Settings: one bridge switch, then which browser does the work (#1332) "Browser bridge" and "Bridge browser" as two toggles read as anagrams. There is one feature and one real decision — which browser drives claude.ai — so the page shows the bridge switch and, under it, a two-way choice: a browser the daemon runs (recommended, with its status and window controls) or your own Chrome (with the token panel). Same boolean preference underneath.
1 parent 5bacc48 commit 24bc66b

42 files changed

Lines changed: 1728 additions & 30 deletions

Some content is hidden

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

FEATURES-SPEC.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,13 @@ happens while nobody is at the keyboard.
167167
- Saved remote devices: run an agent on another machine's daemon — dashboard here, agents there
168168
- Run on a fresh GitHub Actions runner
169169
- Run on a Claude Code cloud session
170-
- A web run's cloud session is created by the Chrome extension in your own browser, through claude.ai's repository picker — repo-bound, so it can push and open its pull request; without the extension (or with the browser bridge off) the run stops and says which is missing
170+
- A web run's cloud session is created by the Chrome extension in your own browser, or in the daemon's bridge browser — through claude.ai's repository picker — repo-bound, so it can push and open its pull request; without the extension (or with the browser bridge off) the run stops and says which is missing
171171
- A web run runs on the model chosen for it — the launcher's or Settings' model is picked in claude.ai's model menu before the session is sent, and a model the menu does not offer stops the run saying so
172172
- Chrome extension bridging claude.ai questions back to the dashboard
173-
- One pinned "The Framework Driver" tab serves every recent cloud session: it reads claude.ai's own session list (the status beside each session), visits a session when the list's word for it changed to awaiting input, unread or idle, an awaiting one again every five minutes, and any session holding a queued answer — navigating inside the app, one page load a minute — and shows a full-page overlay naming what it is, with collapsible debug logs; closing the tab pauses the bridge until the extension's options page reopens it or the browser restarts
173+
- One pinned "The Framework Driver" tab serves every recent cloud session: it reads claude.ai's own session list (the status beside each session), visits a session when the list's word for it changed to awaiting input, unread or idle, an awaiting one again every five minutes, and any session holding a queued answer — navigating inside the app, one page load a minute — and shows a full-page overlay naming what it is, with collapsible debug logs; the overlay stands aside on claude.ai's sign-in page so a person can sign in there; closing the tab pauses the bridge until the extension's options page reopens it or the browser restarts
174174
- The extension's options page shows what its last cycle did, or why it did nothing — including that it reloaded itself because its files changed on disk — and keeps the line current while the page is open
175175
- The extension reloads itself when its files change on disk: an edit in the checkout is running within half a minute, never mid-cycle, with no trip to chrome://extensions
176+
- Bridge browser: the daemon runs its own Chrome for Testing for the bridge — downloaded once, the extension installed and handed the token by the daemon, the Driver tab opened, the window kept minimized — so web runs are created and their questions noticed without your own Chrome being open; Settings presents it as the choice of which browser does the bridge.s work (this one, recommended, or your own Chrome with the token panel), shows what it is doing (the download's progress, running, or stopped and why), brings the window up for the one-time claude.ai sign-in and hides it again, and restarts it on request; a browser a dead daemon left behind is stopped first, and an answer is handed to one Driver at a time so a session served by both browsers is never answered twice
176177
- A cloud run's row says "waiting" when claude.ai's session list shows its session awaiting input, even when the question was asked in prose rather than as a choice block
177178
- A cloud session's conversation mirrored into the run view, turn by turn, as it is written
178179
- Answer a cloud agent's question from the dashboard (typed back into claude.ai) — the same gate panel a local agent gets, multi-select and stop options included, listed with every other open question

packages/chrome-extension/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ the bridge off it simply waits on claude.ai for an answer typed there.
1717

1818
## Set it up
1919

20+
The short way (#1332): turn the bridge on in The Framework's settings, then switch on **Bridge
21+
browser** below it. The daemon downloads a Chrome for Testing (once, ~150 MB), installs this
22+
extension into it over CDP, hands it the token, and keeps the window minimized. Press *Show the
23+
window*, sign in to claude.ai in it once, hide it again — done, and web runs no longer need your
24+
own Chrome open. The steps below are the manual way, for running the extension in your own Chrome.
25+
2026
1. **Turn the bridge on in The Framework.** It is off by default; it opens the daemon's one route
2127
reachable from another origin, so it is an explicit choice.
2228
2. **Load the extension**: `chrome://extensions` -> Developer mode -> Load unpacked -> this directory.

packages/chrome-extension/SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The extension half of the Claude web bridge: a Chrome extension that carries the question a cloud session is parked on into the user's local dashboard, and types the answer given there back into the session on claude.ai.
22

3-
It exists because an agent with run target `web` is hands-off: the daemon hands the whole task to a cloud session on claude.ai and the agent ends at the hand-off. When that session later parks on a gate, nothing streams back — the question is stranded on a claude.ai page nobody may be looking at. The daemon cannot reach claude.ai itself, but the user's own browser is already signed in there, so the extension turns that browser into the bridge's far end, with the daemon's `/_bridge/*` endpoints as the near end.
3+
It exists because an agent with run target `web` is hands-off: the daemon hands the whole task to a cloud session on claude.ai and the agent ends at the hand-off. When that session later parks on a gate, nothing streams back — the question is stranded on a claude.ai page nobody may be looking at. The daemon cannot reach claude.ai itself, but a browser signed in there can, so the extension turns such a browser into the bridge's far end, with the daemon's `/_bridge/*` endpoints as the near end. That browser is either the user's own Chrome, set up by hand, or the daemon's bridge browser — a Chrome for Testing the daemon launches with this extension installed and set up by itself (see the daemon's bridge browser specification); the extension behaves the same in both.
44

55
A cloud session asks the same way a local agent does — its gates are never framed away. The bridge is what carries the question home; with the bridge off, the question simply waits in the session on claude.ai until the user finds it there.
66

@@ -78,7 +78,7 @@ See `## User story`, first item — and it has to hold for fifty sessions as it
7878

7979
The extension only sees pages it is injected into, so it cannot know an agent started. The daemon publishes which cloud sessions are its — every web run's session of the last twelve hours, each flagged with whether an answer is queued — and the extension keeps one pinned, inactive background tab, the Driver, open on claude.ai: content scripts run in background tabs, so the bridge works while Chrome merely runs. Twice a minute the Driver reads the session list — each of the daemon's sessions with the status claude.ai shows beside it — reports those statuses to the daemon, and visits only the sessions the planner picks — a handful a cycle, answers first: the ones whose status changed to a stopped one (awaiting input, unread or idle), the awaiting ones not looked at for five minutes, and any holding a queued answer. A visit is an in-app navigation, clicking the session's row and later the list's "New" link back, never a page load; the list itself is refreshed by one page load a minute, since claude.ai's list refreshes only on a load. A visited session is read and mirrored as any page is, and typed into when an answer travelled with the visit. Running the Driver is opt-in from the options page. Closing the Driver tab pauses the bridge until the options page reopens it or the browser restarts; the user's own claude.ai tabs are never navigated or typed into — with one exception the extension cannot avoid: after a browser restart, a lone pinned claude.ai sessions tab is taken to be the Driver Chrome restored, since a restored tab carries no other mark.
8080

81-
The Driver tab shows a full-page overlay in place of claude.ai's interface: a heading naming it "The Framework Driver", a line saying what the tab is for and that closing it pauses the bridge, and a collapsed "Show debug logs" holding the cycle log — what was read, visited, typed and created, and what the daemon answered.
81+
The Driver tab shows a full-page overlay in place of claude.ai's interface: a heading naming it "The Framework Driver", a line saying what the tab is for and that closing it pauses the bridge, and a collapsed "Show debug logs" holding the cycle log — what was read, visited, typed and created, and what the daemon answered. On claude.ai's sign-in pages the overlay stands aside, so a person can sign in there (the daemon's bridge browser is signed in exactly that way, once), and it returns with the next page.
8282

8383
#### Rationale
8484

packages/chrome-extension/check.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,18 @@ function appPage({ sessions = SESSIONS, firstPage = 6, sendAppendsRow = true } =
717717
const back = Boolean(d.getElementById('tf-driver-overlay'))
718718
if (!back) failed++
719719
console.log(`${back ? 'PASS' : 'FAIL'} the overlay is re-asserted after being removed (back=${back})`)
720+
// The sign-in page is where a person has to act in this tab (the daemon's bridge browser is
721+
// signed in there, once): the overlay stands aside on it and returns with the next page.
722+
w.history.pushState({}, '', '/login?from=logout')
723+
d.getElementById('main').insertAdjacentHTML('beforeend', '<p>sign in</p>')
724+
await new Promise(resolve => setTimeout(resolve, 600))
725+
const aside = !d.getElementById('tf-driver-overlay')
726+
w.history.pushState({}, '', '/code')
727+
d.getElementById('main').insertAdjacentHTML('beforeend', '<p>signed in</p>')
728+
await new Promise(resolve => setTimeout(resolve, 600))
729+
const returned = Boolean(d.getElementById('tf-driver-overlay'))
730+
if (!aside || !returned) failed++
731+
console.log(`${aside && returned ? 'PASS' : 'FAIL'} the overlay stands aside on the sign-in page and returns on the next page (aside=${aside}, returned=${returned})`)
720732
dom.window.close()
721733
}
722734

packages/chrome-extension/content.SPEC.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ A `web`-target agent hands its task to a cloud session and ends, so when that se
2929
- **It watches the page rather than polling it** - the session's own changes trigger a re-read immediately; a slow heartbeat is only a backstop.
3030
- **In the Driver tab it reads the session list** - each of the daemon's sessions becomes a status by the label beside its row (awaiting input, unread, idle, running, landed, or an unknown label carried verbatim; a row showing only a pull request is landed when that pull request is merged or closed and idle in any other state, open or draft), paging the list through its own "Show more" button until every session is found or the list ends; a session not on it is reported missing.
3131
- **Visits happen inside the app** - a session is visited by clicking its row and, afterwards, the "New" link back to the list, never by loading a page; a visited session is surveyed and mirrored like any other — once its own transcript is on the page, not the previous session's — and an answer that travelled with the visit is typed and counted as sent only once the page took the send.
32-
- **The Driver overlay** - the Driver tab is covered by a full-page overlay naming it, saying what it is for, and holding the cycle log behind a collapsed "Show debug logs"; it is re-asserted after any page change and replaces the bridge panel there.
32+
- **The Driver overlay** - the Driver tab is covered by a full-page overlay naming it, saying what it is for, and holding the cycle log behind a collapsed "Show debug logs"; it is re-asserted after any page change and replaces the bridge panel there — except on claude.ai's sign-in pages, where a person has to act, so it stands aside until the next page.
3333
- **One instruction at a time** - a list read or a drive still running makes the next instruction a refusal, never a second drive over the first.
3434

3535
## Business logic
@@ -164,7 +164,7 @@ Reading the list: the list is fetched after the page reports loaded, so it is wa
164164

165165
Visiting: each visit the worker asks for is made by clicking the session's row, waiting for the page's address to name that session and for transcript rows that are not the previous session's to appear — the address changes before the page does — then running the survey, so the parked question and the transcript reach the daemon exactly as from any page. When an answer travelled with the visit, it is typed and submitted the way any answer is, and then the page is given a while to take the send: the delivery counts as sent only once the composer is empty again and the transcript holds a turn row it did not before — a new row, not a higher count, since a long transcript keeps only its tail rendered — and otherwise is reported as failed, naming what the page did. A session not on the list is not visited and its answer is left untouched. When the worker also asked for a session to be created, that comes first, on the list page the cycle starts from and through the same steps as any creation — a run is waiting on it; then the visits; then the "New" link takes the page back to the list without a load. The worker gets back what the creation did, what each visit found, and what each delivery did. While a list read or a drive is running, a second instruction is refused as busy rather than run alongside it — two drives in one page would navigate over each other; it happens when the worker that sent the first ended mid-cycle, since the page drives on regardless.
166166

167-
The overlay covers the whole page: a heading "The Framework Driver", one line saying the tab is used by The Framework to watch the user's Claude Code sessions and type their answers, that another tab should be used for claude.ai, and that closing this one pauses the bridge, then a status line and a collapsed "Show debug logs" holding the cycle log — every list read, visit, delivery and creation with its outcome. It is placed beside claude.ai's interface rather than inside it, so an in-app navigation leaves it alone, and it is put back after any page change that removed it. Every log line is also sent to the worker, which keeps the worker awake for as long as a cycle runs.
167+
The overlay covers the whole page: a heading "The Framework Driver", one line saying the tab is used by The Framework to watch the user's Claude Code sessions and type their answers, that another tab should be used for claude.ai, and that closing this one pauses the bridge, then a status line and a collapsed "Show debug logs" holding the cycle log — every list read, visit, delivery and creation with its outcome. It is placed beside claude.ai's interface rather than inside it, so an in-app navigation leaves it alone, and it is put back after any page change that removed it. On claude.ai's sign-in page, and the sign-out step that redirects to it, the overlay is taken down and not drawn: that is the one place a person has to act in this tab — the daemon's bridge browser is signed in there, once — and it returns with the next page. Every log line is also sent to the worker, which keeps the worker awake for as long as a cycle runs.
168168

169169
#### Rationale
170170

packages/chrome-extension/content.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,15 @@ const MAX_LIST_PAGES = 10
892892
const LOG_LINES = 300
893893
const OVERLAY_ID = 'tf-driver-overlay'
894894

895+
/** claude.ai's sign-in page, and the sign-out step that redirects to it: where a person must act. */
896+
const signInPage = () => /^\/(login|logout)(\/|$)/.test(location.pathname)
897+
898+
/** Take the overlay down, if it is up. */
899+
function removeOverlay() {
900+
const overlay = document.getElementById(OVERLAY_ID)
901+
if (overlay) ownWrites(() => overlay.remove())
902+
}
903+
895904
let isDriver = false
896905
const driverLines = []
897906

@@ -1115,7 +1124,12 @@ async function drive({ visits = [], start }) {
11151124
* through the pointer, so covering the composer costs nothing.
11161125
*/
11171126
function ensureOverlay() {
1118-
if (!isDriver || document.getElementById(OVERLAY_ID)) return
1127+
if (!isDriver) return
1128+
// The sign-in pages are the one place a person has to act in this tab (the daemon's bridge
1129+
// browser is signed in exactly there, once), so the overlay stands aside on them and comes back
1130+
// with the next page.
1131+
if (signInPage()) return removeOverlay()
1132+
if (document.getElementById(OVERLAY_ID)) return
11191133
const overlay = document.createElement('div')
11201134
overlay.id = OVERLAY_ID
11211135
overlay.style.cssText = [
@@ -1148,6 +1162,7 @@ function ensureOverlay() {
11481162
}
11491163

11501164
function renderOverlay() {
1165+
if (signInPage()) return removeOverlay()
11511166
const overlay = document.getElementById(OVERLAY_ID)
11521167
if (!overlay) return
11531168
const version = typeof chrome !== 'undefined' && chrome.runtime?.getManifest ? chrome.runtime.getManifest().version : '?'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
The settings line for the bridge browser: where the daemon's own browser stands, and the one thing only a person can do for it — sign in to claude.ai, once, in its window.
2+
3+
## User story
4+
5+
- The user switches the bridge browser on and wants to know what is happening while its first launch downloads a browser.
6+
- The user has to sign in to claude.ai in the bridge browser once, and needs the window brought up for that.
7+
- The user quit the bridge browser, or it crashed, and wants it back.
8+
9+
## Business logic — TL;DR
10+
11+
- **The line follows the daemon** - it is re-read every few seconds while the switch is on, and shows nothing while it is off.
12+
- **A launch names its step** - so a minutes-long download does not read as a hang.
13+
- **A running browser can be shown and hidden** - shown for the sign-in, hidden again after; the button offered is the one that applies.
14+
- **The sign-in is asked for by name** - when the browser's Driver tab reports claude.ai's sign-in page, the line says to show the window and sign in once.
15+
- **A stopped browser says why, and offers a restart** - the reason the daemon recorded, and a Restart button.
16+
17+
## Business logic
18+
19+
### The line follows the daemon
20+
21+
#### User story
22+
23+
See `## User story`: everything about the bridge browser happens out of sight.
24+
25+
#### Business logic
26+
27+
While the bridge browser switch is on, the line reads the bridge browser's status from the daemon every few seconds. While the switch is off nothing is shown and nothing is read.
28+
29+
### A launch names its step
30+
31+
#### User story
32+
33+
The user switches the bridge browser on for the first time; a browser is being downloaded.
34+
35+
#### Business logic
36+
37+
While the daemon reports the browser as starting, the line says so and quotes the step the daemon is on — the lookup, the download and its percentage, the extension's install, the token hand-over.
38+
39+
### A running browser can be shown and hidden
40+
41+
#### User story
42+
43+
The user needs the window, then wants it gone.
44+
45+
#### Business logic
46+
47+
While the browser runs, the line says so and whether its window is shown or minimized. A minimized browser offers to show the window; a shown one offers to hide it. Either way a Restart is offered. Each button asks the daemon for that action.
48+
49+
### The sign-in is asked for by name
50+
51+
#### User story
52+
53+
The bridge browser is running but signed out, so nothing it does reaches a session.
54+
55+
#### Business logic
56+
57+
When the daemon reports the browser running and its claude.ai tab on the sign-in page, the line says so and to show the window, sign in once, then hide it again; the show button is worded for the sign-in. Once the tab is on any other page, the prompt is gone. The daemon reads that page from its own browser, so the user's own Chrome reporting to the bridge at the same time cannot mask it.
58+
59+
### A stopped browser says why, and offers a restart
60+
61+
#### User story
62+
63+
See `## User story`: the browser was quit, or could not start.
64+
65+
#### Business logic
66+
67+
When the daemon reports the browser as stopped, the line quotes the reason — the exit signal, or the launch step that failed — and offers a Restart, which asks the daemon to launch it again.
68+
69+
## Before modifying/creating SPEC.md files
70+
71+
You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md

0 commit comments

Comments
 (0)