Skip to content

fix(native): cdp guard loss-window backstop, history-navigation race, gate driver plumbing - #17

Merged
unohee merged 4 commits into
masterfrom
fix/cdp-gate-driver-plumbing
Oct 26, 2026
Merged

unohee merged 4 commits into
masterfrom
fix/cdp-gate-driver-plumbing

Conversation

@unohee

@unohee unohee commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

These changes clear the last blockers before 0.2.0, which makes native the default driver (AGT-4829).

Changes

  • f8be29d verify_cdp_guard_e2e.py now takes --driver, and defaults it to LYRA_BROWSER_DRIVER. Its route control stays pinned to Playwright. browser-gates.yml passes the job's driver to the gate.
  • e4f7975 Native go_back and reload no longer settle on the previous document's late frameStoppedLoading. _Outcome.started is set on Page.frameStartedLoading.
  • e0436e8 Native adds a backstop interceptor on its own DevTools connection. It lets a request through only if the cdp guard vouched for it, which closes the window between losing the guard and killing the browser.

Measurements

Measured with a hostile page that POSTs every 4 ms.

Natural loss windows:

Driver Windows with a leak
native, before the backstop 1 of 40
playwright 3 of 40
native, with the backstop 0 of 40

Slow-kill test, with a 300 ms gap between losing the guard and killing the browser:

Driver Windows with a leak
playwright 6 of 6 (32–51 POSTs each)
native, with the backstop 0 of 6

Regression gates: browser 31/31, redirect_hop 177/177, tabs 57/57, downloads 99/99.

Unit tests: 1819 passed.

…he driver from the job

The Linux nightly run showed three sections failing with 'backend is not route' /
'wrong guard on session' on both jobs: the gate's route control was built with
driver auto (or native, from LYRA_BROWSER_DRIVER), which lands on native and
always runs the cdp guard. A route control now launches Playwright, the driver a
rig asks for is set for the build (driver_env), --driver defaults to
LYRA_BROWSER_DRIVER, and the workflow passes it.

cannot_start injected its failure at read_endpoint, which the native driver never
calls (it reads DevToolsActivePort at launch and hands the endpoint over), so on
native the sidecar started and open_browser said ok. The seam is now the sidecar's
start on native, read_endpoint otherwise.
…e stop

go_back/reload settled their outcome on any frameStoppedLoading. The document a
goto had just loaded reports its own stop a tick after its load event, so a
go_back or reload sent right after a goto was 'stopped' (ERR_ABORTED) before
Chrome had begun it, with the guard not yet having refused anything: the go_back
tool re-raised instead of answering blocked_by_policy, and a refused reload left
no audit row.

The outcome now counts a stop only after this navigation's frameStartedLoading.
Measured on cdp_guard's speculative section (back/reload with the HTTP cache on):
native went from 1 of 3 runs to 9 of 10, the remaining run failing only on a
process still exiting; Playwright 4 of 4 under the same conditions. Unit tests
fire the old document's stop right after the command and assert it stays pending.

The gate also notes which process outlived its 6 s wait off Linux, where the
leftover check now runs for the first time.
When the cdp guard's connection ends, the session kills the browser, but anything
the page sends in between meets no guard. Measured over 40 loss windows per driver
(a hostile page POSTing every 4 ms): native 1 leaked, Playwright 3 — a limit of
the cdp guard, not of either driver. Default-native makes the cdp guard the
default, so this was worth closing.

Native has a second DevTools connection of its own, which outlives the guard's.
Each tab now gets a Document interceptor there (enable_request_filter). Chrome
puts it after the guard's (measured: it sees only what the guard let through),
and it lets a request go only if the guard vouched for it: CdpGuard records each
allowed (networkId, url) before answering, and vouched() spends it once. A request
that reaches the backstop unvouched was never judged — the guard was gone — and
is refused with a 204.

Measured with the loss-to-kill gap widened to 300 ms so the window always opens:
Playwright leaked in 6 of 6 windows (32-51 POSTs each), native in 0 of 6. Natural
windows: 0 of 40 on native. Regressions: browser 31/31, redirect_hop 177/177,
tabs 57/57, downloads 99/99; unit tests 1819 passed (vouch spent once, never for
refused or unseen requests, per redirect hop).
…d on Playwright 1.64

Both failed on the Linux gate job (run 40250051727), neither from a guard defect.

workers_sw: Playwright 1.64 blocks service workers by replacing
ServiceWorkerContainer.prototype.register in an init script (coreBundle.js), so
the fixture's prototype call resolved undefined on both drivers. The worker is
now registered from a CDP isolated world, which has prototypes of its own. On
Playwright, title() of a refused goto can report Chrome's pending entry
("Loading <url>"); the opener check accepts it, since the URL and AWAY's empty
request log already show the document is unchanged. Measured on macOS: native
8/8, playwright 8/8.

wire: native drives Chrome over the debugging port, so it has no
--remote-debugging-pipe, and the route control is a Playwright launch, so the
command-line diff against it is noted rather than asserted for native.
@unohee
unohee merged commit 8bee95e into master Oct 26, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant