Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
18bbbb3
Validate ModCDP protocol surfaces
pirate May 15, 2026
44c7a66
Fix ModCDP CI checks
pirate May 15, 2026
4539f17
Fix borrowed injector bootstrap lookup
pirate May 15, 2026
4cb1ef9
Allow demos more Chrome startup time
pirate May 15, 2026
463ee5a
Allow proxy examples more Chrome startup time
pirate May 15, 2026
fca3051
Stabilize ModCDPClient browser tests
pirate May 15, 2026
c4ec952
Align Python ModCDPClient browser tests
pirate May 15, 2026
c40259a
Use Python auto injector path in browser test
pirate May 15, 2026
06becd2
Fix Python pipe browser launch
pirate May 15, 2026
313f12b
Pass Python pipe fds directly
pirate May 15, 2026
6008edb
Stabilize Python pipe fd setup
pirate May 15, 2026
b810472
Align remote close browser tests
pirate May 16, 2026
2dfa879
Stabilize reverse transport CI paths
pirate May 16, 2026
943f269
Give router live context tests CI time
pirate May 16, 2026
b3f853b
Avoid parent fd mutation for Python pipe launch
pirate May 16, 2026
8cd3d8f
Use posix spawn for Python pipe launch
pirate May 16, 2026
e07e4dd
Allow remote close test full browser budget
pirate May 16, 2026
67b466d
Set remote close test timeout
pirate May 16, 2026
566e1f0
Correct remote close timeout placement
pirate May 16, 2026
bd87af3
Use injector path in remote close tests
pirate May 16, 2026
bfe7ea9
Preload extension for remote close tests
pirate May 16, 2026
583fa3c
Use reversews browser helper in Python client test
pirate May 16, 2026
2326b88
Use reversews browser helper in Go client test
pirate May 16, 2026
965fe7f
Bump ModCDP versions
pirate May 16, 2026
a8c8fe9
Fix launcher sandbox defaults
pirate May 16, 2026
77f930d
Stabilize Python pipe fd mapping
pirate May 16, 2026
5ad98ca
Map Python pipe fds in child process
pirate May 16, 2026
1765b4f
Pass custom command params as runtime arguments
pirate May 16, 2026
d3dd139
Fix Python translate test typing
pirate May 16, 2026
cc29459
Stabilize extension injection fallback timing
pirate May 16, 2026
844719d
Isolate reversews CI tests
pirate May 16, 2026
bf4ec49
Make pipe demos deterministic in headless CI
pirate May 16, 2026
c53d2c1
Bump ModCDP to 0.0.19
pirate May 16, 2026
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
50 changes: 33 additions & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: |
case "${{ matrix.client }}" in
js)
xvfb-run -a pnpm exec vitest run \
pnpm exec vitest run \
$(find js/test -name 'test.*.ts' \
! -name 'test.ModCDPClient.ts' \
! -name 'test.NatsUpstreamTransport.ts' \
Expand All @@ -52,7 +52,7 @@ jobs:
;;
python)
cd python
xvfb-run -a uv run python -m unittest \
uv run python -m unittest \
$(find tests -name 'test_*.py' \
! -name 'test_ModCDPClient.py' \
! -name 'test_NatsUpstreamTransport.py' \
Expand All @@ -61,7 +61,7 @@ jobs:
;;
go)
cd go
xvfb-run -a go test -count=1 -p 1 \
go test -count=1 -p 1 \
./modcdp \
./modcdp/injector \
./modcdp/launcher \
Expand Down Expand Up @@ -122,15 +122,15 @@ jobs:
run: |
case "${{ matrix.client }}" in
js)
xvfb-run -a node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
python)
cd python
xvfb-run -a uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
go)
cd go
xvfb-run -a go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }}
;;
*)
echo "unknown client: ${{ matrix.client }}" >&2
Expand Down Expand Up @@ -164,25 +164,41 @@ jobs:
cache-dependency-path: go/go.sum
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Run JS serialized connector tests
- name: Run JS serialized non-reverse connector tests
run: |
xvfb-run -a pnpm exec vitest run \
pnpm exec vitest run \
js/test/test.ModCDPClient.ts \
js/test/test.NatsUpstreamTransport.ts \
js/test/test.proxy.ts \
--testNamePattern "^(?!.*reversews).*" \
--fileParallelism=false --maxWorkers=1
- name: Run JS serialized reversews tests
run: |
pnpm exec vitest run \
js/test/test.ReverseWebSocketUpstreamTransport.ts \
js/test/test.proxy.ts \
--testNamePattern "reversews" \
--fileParallelism=false --maxWorkers=1
- name: Run Python serialized connector tests
- name: Run Python serialized non-reverse connector tests
run: |
cd python
xvfb-run -a uv run python -m unittest \
uv run python -m unittest \
tests.test_ModCDPClient \
tests.test_NatsUpstreamTransport \
tests.test_NatsUpstreamTransport
- name: Run Python serialized reversews tests
run: |
cd python
uv run python -m unittest \
tests.test_ReverseWebSocketUpstreamTransport
- name: Run Go serialized connector tests
- name: Run Go serialized non-reverse connector tests
run: |
cd go
go test -count=1 -p 1 ./modcdp/client
go test -count=1 -p 1 ./modcdp/transport -run 'Test(UpstreamTransport|WebSocketUpstreamTransport|PipeUpstreamTransport|NativeMessagingUpstreamTransport|NatsUpstreamTransport)'
- name: Run Go serialized reversews tests
run: |
cd go
xvfb-run -a go test -count=1 -p 1 ./modcdp/client ./modcdp/transport
go test -count=1 -p 1 ./modcdp/transport -run 'TestReverseWebSocketUpstreamTransport'

serialized-reversews-demo:
name: serialized reversews demos
Expand Down Expand Up @@ -213,12 +229,12 @@ jobs:
- name: Run reversews demos serially
run: |
for mode in loopback debugger; do
xvfb-run -a node dist/js/examples/demo.js --"$mode" --upstream=reversews
node dist/js/examples/demo.js --"$mode" --upstream=reversews
cd python
xvfb-run -a uv run python examples/demo.py --"$mode" --upstream=reversews
uv run python examples/demo.py --"$mode" --upstream=reversews
cd ..
cd go
xvfb-run -a go run ./examples/demo --"$mode" --upstream=reversews
go run ./examples/demo --"$mode" --upstream=reversews
cd ..
done

Expand Down Expand Up @@ -247,4 +263,4 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: xvfb-run -a ${{ matrix.command }}
- run: ${{ matrix.command }}
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ Native messaging mode creates the local native host wrapper and browser manifest
Use reverse mode when the browser does not expose a public CDP websocket to the final client, but the ModCDP extension can open a websocket back to a local proxy. The proxy still serves a normal-looking CDP endpoint to Playwright, Puppeteer, Stagehand, or any other CDP client:

```sh
pnpm run proxy -- --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --listen 127.0.0.1:9223
pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --port 9223
pnpm run proxy -- --upstream-mode=reversews --port 9223
pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --port 9223
pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29293 --port 9223
# const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223")
```

Reverse mode is opt-in. The shipped extension has no generated runtime config; it always tries the fixed local reverse connector at `ws://127.0.0.1:29292`. With `--launcher-mode=local`, use that default bind and the normal `ModCDPClient` launcher, injector, and `ReverseWebSocketUpstreamTransport` path will wake the service worker and accept its connection. With `--launcher-mode=none` or a non-default bind, an already-running extension or test must explicitly call `globalThis.ModCDP.startReverseBridge("ws://host:port", { reconnect_interval_ms: 2000 })` with the chosen endpoint because there is no side channel before the reverse connection exists. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits for that extension connection. Once it connects, it self-identifies as a ModCDP extension service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.

Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`.

Expand Down Expand Up @@ -230,7 +231,7 @@ dist/ Built JS output used by the extension and Node CLI scr
3. Attach a session to that SW target and `Runtime.enable` on it.
4. Call `globalThis.ModCDP.configure(...)` to push the resolved loopback websocket and any explicit server route overrides into the SW. The clients do this automatically by default.

Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292` listens for the extension, while still serving downstream clients from `--listen`. The extension artifact is fixed, so automatic launch/injection uses the default `127.0.0.1:29292` connector; non-default reverse binds need an external bootstrap call to `globalThis.ModCDP.startReverseBridge(...)` with the same URL. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.
Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --port 9223` listens for the extension on the configured reverse connector while still serving downstream clients from the proxy port. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client.

### Send

Expand Down
2 changes: 0 additions & 2 deletions TODO_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ repo/
pages/
options.html
options.ts
wake.html
wake.ts
offscreen_keepalive.html
offscreen_keepalive.ts

Expand Down
2 changes: 1 addition & 1 deletion extension/src/pages/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
<button id="refresh">Refresh</button>
<h1>ModCDP</h1>
<pre id="out">Loading...</pre>
<script src="options.js"></script>
<script type="module" src="options.js"></script>
10 changes: 0 additions & 10 deletions extension/src/pages/wake.html

This file was deleted.

3 changes: 0 additions & 3 deletions extension/src/pages/wake.ts

This file was deleted.

34 changes: 21 additions & 13 deletions extension/src/service_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ const downstreamClient = (session_id?: string | null) => {
last_seen: at,
});
const id = session_id || "root";
const session = (client.sessions[id] ??= { id, commands: 0, events: 0, first_seen: at, last_seen: at });
const session = (client.sessions[id] ??= {
id,
commands: 0,
events: 0,
first_seen: at,
last_seen: at,
});
return { at, client_id, client, session };
};
const logTraffic = (direction: "command" | "event", name: string, payload: unknown, session_id?: string | null) => {
Expand Down Expand Up @@ -156,7 +162,11 @@ if (bridge) {
if (start) {
bridge[method] = (...args: unknown[]) => {
const result = start(...args);
self_transports[key] = { args: compact(args), result: compact(result), updated_at: new Date().toISOString() };
self_transports[key] = {
args: compact(args),
result: compact(result),
updated_at: new Date().toISOString(),
};
return result;
};
}
Expand Down Expand Up @@ -190,15 +200,6 @@ chrome.runtime.onInstalled.addListener(startConfiguredTransports);
chrome.runtime.onStartup.addListener(startConfiguredTransports);

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === "modcdp.wake") {
startConfiguredTransports();
sendResponse({
ok: true,
extension_id: chrome.runtime.id,
service_worker_url: chrome.runtime.getURL("modcdp/service_worker.js"),
});
return false;
}
if (message?.type !== "modcdp.options.status") return false;
const self = {
id: "self",
Expand All @@ -221,7 +222,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
native_bridge_last_error: bridge.native_bridge_last_error,
},
...(Object.keys(self_transports).length ? { transports: self_transports } : {}),
custom: { commands: [...self_custom.commands], events: [...self_custom.events] },
custom: {
commands: [...self_custom.commands],
events: [...self_custom.events],
},
log: self_log,
};
sendResponse({
Expand All @@ -240,7 +244,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
},
}
: {}),
debugger: { ...upstream_servers.debugger, id: "debugger", log: upstream_servers.debugger?.log ?? [] },
debugger: {
...upstream_servers.debugger,
id: "debugger",
log: upstream_servers.debugger?.log ?? [],
},
},
});
return false;
Expand Down
Loading
Loading