all repos — sz-chrome-remote-extension @ master

pi-chrome, patched to drive a signed-in browser on another host through a broker, with an MCP server

FORK.md (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
# Fork notes

**sz-chrome-remote-extension**: driving a signed-in browser through its
companion extension, from other machines. A fork of
[tianrendong/pi-chrome](https://github.com/tianrendong/pi-chrome) that lets pi
on **other machines** drive one signed-in browser. It adds a remote mode to the
pi extension, a standalone broker for the browser's host, and `mcp/`, an MCP
server for the same tools. Without `PI_CHROME_REMOTE` set, the pi extension
behaves exactly like upstream. Until 2026-10 this repository was named
pi-chrome.

Not published to npm and not proposed upstream. The package keeps upstream's
name, `pi-chrome`, in `package.json`: renaming it would conflict on every
rebase.

```
origin    https://git.soarez.net/sz/sz-chrome-remote-extension.git
upstream  https://github.com/tianrendong/pi-chrome.git
```

## Why the patch exists

Upstream only works on the machine running the browser:

- the companion extension long-polls a bridge at `127.0.0.1:17318`, hardcoded
  in `browser-extension/service_worker.js`;
- that bridge lives inside whichever pi session started first on the machine,
  and goes away when that session exits;
- a pi session on another host cannot join it. `PI_CHROME_BRIDGE_HOST` set to a
  remote address makes `listen()` fail with `EADDRNOTAVAIL`, and only
  `EADDRINUSE` switches a session into client mode, so the session fails at
  startup.

Upstream tried a standalone daemon in 0.16–0.17 (`docs/DESIGN-0.17-daemon.md` in
the v0.17.3 tag) and then dropped it; main is back on the 0.15 line. This fork
is based on main.

## What the fork changes

**`broker/broker.mjs`** runs on the browser's host in place of upstream's
in-session bridge. It speaks upstream's HTTP contract to the extension, so the
extension is loaded exactly as shipped. Node builtins only, no install step. It
has two listeners:

| listener | address | routes |
|---|---|---|
| extension | `127.0.0.1:17318` | `/next`, `/result`, `/status`, `/command` |
| peer | `PI_CHROME_BROKER_PEER_HOST`, same port | `/command`, `/status` |

Keeping `/next` and `/result` off the peer listener means nothing on the network
can drain the extension's queue or forge results. `/command` needs a bearer
token on both listeners, because the browser is signed into real accounts. Bind
the peer listener to an address on a network you trust (a VPN, say), never to
`0.0.0.0`.

| Broker setting | Effect |
|---|---|
| `PI_CHROME_BROKER_PORT` | both listeners' port; default `17318`, which the extension expects |
| `PI_CHROME_BROKER_PEER_HOST` | the peer listener's address; unset, there is no peer listener |
| `PI_CHROME_TOKEN` | the bearer token |
| `PI_CHROME_TOKEN_COMMAND` | a command whose output is the token, for a secret store |

**`extensions/chrome-profile-bridge/index.ts`**: every change is marked
`FORK:`.

| Setting | Effect |
|---|---|
| `PI_CHROME_REMOTE` unset, or `local` (also `off`, `none`, `0`, `no`, `localhost`) | upstream behaviour |
| `PI_CHROME_REMOTE=host:port` | the broker there |
| `PI_CHROME_TOKEN`, `PI_CHROME_TOKEN_COMMAND` | the bearer token, as for the broker; read on first use |
| `PI_CHROME_AUTHORIZE=indefinite` (or `30m`, `45`…) | skip `/chrome authorize`: every session starts authorized |
| `PI_CHROME_REMOTE_LABEL` | how the prompt names the browser; default "the user's own browser on *host*" |
| `PI_CHROME_REMOTE_SSH` | the ssh target the prompt gives for file transfer; default the broker's host |
| `PI_CHROME_OTHER_BROWSER_TOOLS` | another browser's tools (e.g. `browser_*`), for the prompt to route between |

In remote mode the session never binds a port and never promotes itself to
bridge. If the broker is unreachable the tools fail. They do not silently drive
some other browser on this machine.

The system prompt gains a `pi-chrome-remote` section beside upstream's
`chrome-profile-bridge` primer, present while `chrome_*` is authorized. It is
added by a `before_agent_start` handler of its own, so upstream's handler stays
as shipped. It says `chrome_*` drives
a signed-in browser on another host, and, when another browser's tools are
named, to keep this one for tasks that need the user's sessions. It also covers
moving files: uploads are staged on the browser's host with scp, downloads land
in its `~/Downloads`, and screenshots are already local. `chrome_upload_file`
refuses relative paths in remote mode, because the browser opens them on its own
host.

Pi sessions on different hosts each get their own automation window. The
extension keys windows by pi session id, which is globally unique. Background
mode, which is upstream's default, keeps them from focusing the browser or
switching its tabs.

## MCP clients

`mcp/` serves these tools to Claude Code, Claude Desktop and other MCP clients;
its README has the setup. It loads the pi extension from this repository,
`../extensions/chrome-profile-bridge/index.ts`, so the server, the extension and
the broker always come from one commit.

It was its own repository, pi-chrome-mcp, from 2026-09-25 (when it moved out of
this fork to stand alone, loading pi-chrome from npm) until 2026-10, when it
came back to keep everything that drives the browser through its extension in
one place. Its commits were replayed here with `git am --directory=mcp`, linear,
so they rebase like any fork commit. It touches no upstream file, so it never
conflicts. It keeps its own `package.json` and lockfile; `mcp/run.sh` installs
them into `mcp/node_modules`.

Run it from a checkout of its own, not from pi's package clone
(`~/.pi/agent/git/...`): pi runs `git clean -fdx` there whenever it updates the
package, which deletes `mcp/node_modules`.

## Setup

On the browser's host:

1. Clone this repository somewhere it will stay. The browser disables an
   unpacked extension whose folder goes missing, so not a package manager's
   cache that it may replace.
2. Load the extension once: `chrome://extensions` (or `brave://extensions`) ->
   Developer mode -> Load unpacked ->
   `extensions/chrome-profile-bridge/browser-extension`.
3. Run `node broker/broker.mjs` under a supervisor (launchd, systemd), with
   `PI_CHROME_BROKER_PEER_HOST` and a token set. No pi session on that machine
   may be running stock pi-chrome, which would take the port first.

On every other machine, install the fork as a pi package and set
`PI_CHROME_REMOTE=<browser host>:17318` and the token.

## Taking upstream releases

```
git fetch upstream
git rebase upstream/main
git push --force-with-lease origin master
```

Then update the broker's checkout, restart it, and reload the extension in the
browser. `/chrome doctor` reports a version mismatch until you do.

Commits under `broker/` and `mcp/` never conflict. Expect conflicts in the
places marked `FORK:` in `index.ts`: the bridge class's
bind, forward and status code (including `connectionStatus`, whose `/status`
fetch carries the token), `session_start`, the remote prompt-section handler
just ahead of upstream's primer, doctor, and `chrome_upload_file`. If upstream changes the extension↔bridge HTTP contract
(`/next`, `/result`, the `x-pi-chrome-version` header), port the change to
`broker/broker.mjs`. Its request handling is a line-by-line port of the server
half of `ChromeProfileBridge`.

## Verifying

`/chrome doctor` in a pi session should report "using the pi-chrome broker at
http://*host*:17318" and a connected extension. From outside pi:

```
curl -s -H "Authorization: Bearer $PI_CHROME_TOKEN" http://<host>:17318/status
```

`connected: true` with a recent `lastSeenAt` means the extension is polling.