Playwright 1.61.0 (2026-06-29) ships a Browser.setDefaultViewport Juggler
call the published firefox-13 binary does not implement, so an unpinned
pip install (the wrapper declared playwright>=1.40) breaks new_context on
fresh installs. Cap the dependency to <1.61 and force the blessed pin in
the e2e clean venv so CI tests the version users actually run.
The pool had the bare "franklin gothic", which isn't a real Win11 family and
wouldn't match the bundled franklingothic_x.ttf, so the always-present standard
fonts test failed and the family could drop off the whitelist. Use the real
name (the bundle reconciliation already maps it).
firefox-13 ships the bundled Windows fonts, makes navigator.languages /
Accept-Language / the Intl locale all follow the proxy country, and gates the
audio fingerprint noise off. The prefs.py change that emits the new font/locale
prefs needs this binary, so the bump and that change land together.
locale defaults to "auto" and resolves from the proxy egress country the same
way timezone does - it reuses the egress IP, maps the country to a BCP-47 locale
with the offline mmdb, and falls back to en-US. prefs emit juggler.locale.override
(the full Accept-Language list) so the binary keeps navigator.languages and the
Intl default locale in sync.
The audio fingerprint noise is off in the baseline. Font prefs match the new
binary: the sampled whitelist drives font.system.whitelist, system-ui is Segoe
UI, the bundled fonts are activated, and the CSS generics are pinned to Windows
defaults so they resolve on a non-Windows host too.
- BINARY_VERSION -> firefox-12 (self-calibrating font widths, per-family canvas
distinctness, render-noise that preserves solid reference renders).
- font_pool: the standard Windows fonts (Calibri, Franklin Gothic, Gadugi,
Javanese Text, Myanmar Text) move from the per-profile optional set to core,
so they are always present and the detected font set matches a real Windows
install on every host. Defensive dedup in derive_font_prefs.
- GPU persona applied on every platform (Linux/macOS present a coherent Windows
GPU + WebGL params); pool re-rooted on a real-device GPU mix; render seeds
recalibrated.
- prefs: emit absolute per-family font widths that the binary self-calibrates.
- geoip: always pull the latest mmdb via the releases/latest permalink, checked
each launch, offline-safe (no pinned tag that can 404).
- tests: per-font canvas distinctness, solid-readback purity under render-noise,
always-present standard-font invariant, no duplicate families.
The Date.now self-assignment was a stopgap that the page-realm fix (the
automation layer no longer touches the realm's built-ins before page scripts
run) made unnecessary; that binary support was never landed, so the pref is a
no-op on every shipped build. Remove it and its now-inaccurate comment.
Bump BINARY_VERSION firefox-10 -> firefox-11 so first-run fetches the new
patched build (keeps the automation layer out of the page realm's observable
state + synthetic-cursor input realism). Add the Date.now property
normalization to the baseline prefs, paired with the firefox-11 binary's
zoom.stealth.normalize_date_now support.
FF150+Fission auto-loads about:newtab (TopSitesFeed) ~100ms-1s after a tab's
first navigation — a cross-process BC swap that replaces the page. The wrapper
always disables this (prefs.py); the raw-Playwright drive gate did not, so any
action that adds latency (e.g. the human-cursor motion path) let the page vanish
mid-sequence and surfaced as a phantom "waiting for locator" timeout — an
environment artifact, not a binary defect. The gate now launches with the same
newtab-suppression prefs every real run uses.
Add republish.yml: re-gate + publish an existing build run's artifacts without
rebuilding, for when all builds succeeded but a gate/test bug blocked publish.
Cut the per-seed WebGL persona to the two renderer buckets that score clean on
FP Pro tampering_ml across seeds (AMD Radeon R9 200 Series and Intel Arc A750),
weighted 70/30, cross-vendor so the fleet isn't one fixed GPU. Every NVIDIA
bucket and the integrated/ancient Intel buckets are penalised, so they're out.
The canvas/WebGL render-image hash turned out to be the dominant tampering_ml
driver, not the attributes, so the render-noise seed (zoom.stealth.fpp.hw_seed)
is now decoupled from the identity seed and drawn from a calibrated clean pool.
Per-seed determinism and per-user diversity are preserved.
Also in this change:
- audio maxChannelCount is stereo-dominant per class (it reflects the output
device, not the GPU; the old tables over-emitted 5.1/7.1 surround)
- route discrete Intel Arc desktop cards to a discrete-GPU class (not integrated)
- condition the whole sampled profile on the exposed GPU class via the sampler's
evidence path, so cores/screen/storage stay coherent with the declared GPU
- apply per-named-font width factors on Windows/macOS so canvas measureText
widths don't collapse to a single value
12/12 seeds clean on tampering_ml (worst 0.29), bot and anti-detect negative,
and the fingerprint stays identical across repeated runs of the same seed.
The Playwright/Juggler Firefox build re-enables cipher 0xC009
(TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA), which retail Firefox 150 does not offer.
That extra (17th) cipher shifted our JA3/JA4 off every real Firefox
(ja4 t13d1717h2 / ja3 6f7889b9 vs stock t13d1617h2 / 6447ab08) — a TLS
fingerprint that matches no real browser, which is itself a consistency tell.
Set security.ssl3.ecdhe_ecdsa_aes_128_sha=false in _BASELINE so JA3/JA4/peetprint
are byte-identical to retail FF150 (verified on tls.peet.ws). Non-breaking: stock
Firefox ships without 0xC009 and works on the whole web.
humanize: the wrapper wrote invisible_playwright.humanize[.maxTime], but the
binary's Juggler reads stealthfox.humanize (PageHandler.js gates the Bezier
mouse path on it). The old name was a dead no-op, so humanize never fired and
every mouse.move teleported the cursor — an automation tell. Renamed across
config.py, launcher.py and async_api.py; the mouse test now asserts the on/off
contrast instead of a false-green moves>=1.
headless (async): InvisiblePlaywright(headless=True) crashed on Windows/macOS.
_resolve_headless called make_virtual_display().start() unconditionally, but on
Win/macOS that returns None (the binary self-cloaks via DWMWA_CLOAK; only Linux
spawns Xvfb), so it died with AttributeError. It also never injected
cloak_prefs(), so the window wouldn't have hidden anyway. Mirror the sync
launcher: guard `if vd is not None` + inject cloak_prefs() when headless on
win32/darwin. Verified on FF150: headless=True loads, exits clean, window fully
hidden (no MainWindowHandle / no taskbar entry).
Alongside BotD and FingerprintJS, the detector e2e now runs two more FOSS
client-side detectors against the binary, vendored and served from localhost so it
stays offline on CI. fpscanner: assert its engine-agnostic bot rules (webdriver/
selenium/bot-UA/platform/timezone/language) are clean. CreepJS (the Firefox-aware
one): runs fully offline via window.Fingerprint with every non-loopback request
aborted, asserting headlessRating==0 and no JS-proxy stealth tell. The Chrome-only
and GPU-sensitive signals are logged, not asserted, so a software-WebGL CI host
doesn't false-red. Validated against firefox-10: full e2e 130 green.
The publish job used a fixed body that still read 'DRAFT - do not publish' on the
live release and listed none of the actual changes. Now the body is built from the
source commits that went into the binary: the build records which invisible_firefox
commit it came from (source-commit.txt), and publish diffs that against the previous
release's recorded commit via the GitHub compare API (no deep clone, no cross-repo
token) to list the user-facing subjects. docs/chore/ci/test commits are filtered out,
and the body ends with 'Built from invisible_firefox @<sha>' for traceability. It's
still a draft - the realness gate and the un-draft flip stay manual (issue #14).
firefox-10 is published: the in-binary headless window cloak (Windows DWMWA_CLOAK,
macOS NSWindow alpha-0, Linux keeps Xvfb) and the WebGL readPixels gamma remap that
clears pixelscan's masking flag. Validated against the built binary — validate_release
(linux+win), full e2e (128 passed), fppro ALL CRITICAL CLEAN, consistency PASS, and the
cloak gate green on all 5 targets (macOS via CGWindowAlpha).
test_hover_triggers_mouseenter timed out at the old 5s wait when run as part of
the full e2e suite — browser startup + CPU contention occasionally push the
mouseenter past the window. In isolation the event fires in well under a second
(5/5), so this is load-sensitivity, not a real regression. A 10s wait absorbs it
while still failing fast if mouseenter genuinely never fires.
The firefox-10 build gated green on all 5 targets but both macOS gate legs
failed. The cloak/webgl guards hard-required a live WebGL context, and macOS
GitHub runners expose none in the CI session (no software-GL fallback, unlike
Linux llvmpipe and Windows WARP). The cloak renders fine there anyway, which the
non-blank screenshot proves, so on the mac legs the WebGL-present check now
self-skips and the cocoa cloak is validated via the screenshot plus CGWindowAlpha.
The gamma masking guard skips on mac too (platform-agnostic C++, covered on Linux
and Windows).
verify-cloak.yml re-runs these guards against a prior build run's artifacts with
no rebuild, so a test-only fix like this is validated against the real binaries
in minutes instead of a 3h rebuild.
The gate runner only had Playwright; `pip install -e .` doesn't pull pytest (a
dev dep), so `python -m pytest` failed with "No module named pytest". Install
".[dev]" like e2e.yml does.
headless=True now hides the window via the binary's own cloak pref
(zoom.stealth.cloak_windows) on Windows and macOS instead of the broken
thread-level SetThreadDesktop; macOS is now supported. Linux keeps Xvfb.
Adds e2e guards that also run per-platform in the release drive-gate:
- test_cloak: the window is hidden (Windows DWMWA_CLOAKED / macOS CGWindowAlpha)
yet still renders + drives; the macOS leg is where the cocoa cloak patch runs.
- a WebGL readPixels masking guard: the gamma noise must stay a smooth gamma
remap, not the pixelscan-maskable +-1 spikes.