The free hosted runners (windows-latest worst) are content-process unstable
under a heavy headless interaction sequence: clicks/moves cascade into
context-destroyed / selector-timeout / eval-CSP, even across 3 retries, even on
linux-arm64. That's an environment limit, not a binary defect (the binaries
drive 20/20 locally and the stable legs pass).
So: SMOKE (launch + http page + UA + webdriver + DOM read) runs on all 5 legs —
the firefox-8/juggler catcher, robust everywhere. FULL (+ mouse/keyboard/canvas/
navsurface, the firefox-2 class) runs only on linux-x86_64; the interaction code
is platform-identical JS (omni.ja), so one reliable full run covers every
platform, and win interaction stays covered by local pre-release testing.
windows-latest headless kept flaking on special-scheme pages: data: URLs get
re-normalized (re-nav), about:blank + redundant goto destroys the context, and
both can carry a CSP that blocks eval(). Serve the test page over a real
http://127.0.0.1 instead — none of those quirks, and it adds real-navigation
coverage (await a response). Evaluates stay arrow-functions (no eval), listeners
are inline-script (no on* attrs), and transient context-destroyed/detached/timeout
gets up to 2 retries. A genuinely broken binary fails all 3 attempts.
windows-latest failed both attempts with "Page.evaluate: call to eval() blocked
by CSP". Bare-string evaluates (page.evaluate("navigator.userAgent")) make
Playwright fall back to eval(), which a page CSP blocks. Pass arrow functions
instead (called via callFunction, never eval'd), and wire the click listener
with addEventListener instead of an inline onclick (also CSP-sensitive). Both
are Playwright best practice and platform-agnostic.
linux+macOS drive went green but windows-latest kept throwing "execution
context destroyed by navigation" at a wandering evaluate (passed 20/20 win-local,
no browser crash logged). Root cause: the unencoded data: URL gets re-normalized
(re-navigated to its percent-encoded form) by Firefox; the slower win runner
races that re-nav against the evaluates. about:blank is canonical and never
re-navigates, so the DOM is now built there via innerHTML. Also add one logged
retry on transient context-destroyed/detached (a broken binary fails both).
Re-running the enriched gate on the real binaries exposed a ~1-in-5 flake: the
iframe probe (nested data: / srcdoc) re-navigates, so Juggler's frame id changes
mid-check ("execution context destroyed" / "Frame was detached"). set_content
isn't an option either — this build rejects its document.write ("operation is
insecure").
Drop the iframe from the gate: a same-origin srcdoc iframe is a weak proxy for
the cross-origin issue #20 anyway. The page is now a plain subframe-free
goto(data:), and the mouse/keyboard/canvas-determinism/navigator-surface checks
(the firefox-2 class + stealth smoke) stay. 20/20 clean locally. The faithful
#20 sentinel (tests/test_cross_origin_iframe.py, two localhost origins) should
be wired as its own e2e gate job.
An adversarial audit of the pipeline found the drive gate only did goto+evaluate,
so several historically-shipped breakages would still pass it green:
- firefox-2 (jugglerSendMouseEvent missing) — no mouse/keyboard was tested
- issue #20 (cross-origin iframe content_frame() None) — no iframe was tested
- canvas non-determinism (stealth seed) and headless navigator tells
ci_drive_gate.py now clicks a button, moves the mouse, types into an input,
reaches into an iframe, checks an identical canvas draw is byte-stable, and
checks navigator.languages/plugins — all offline (data: URLs), GPU-free, no
proxy. Validated against the real build.
Pipeline hardening from the same audit:
- Windows: stop swallowing `mach package` failure and never fall back to the
dev tree dist/bin (that masked the firefox-7/8 packaging bugs)
- macOS: plutil -lint Info.plist + required-key checks (a malformed plist ships
fine through a headless drive but Finder calls the .app "damaged")
- publish: assert all 5 archives present + fail_on_unmatched_files (no silent
partial release if a build leg drops out)
gh release download 404s ("release not found") on a draft tag when the token
is contents:read — GitHub only shows drafts to tokens with push access. The
workflow still only reads assets; the scope bump is purely for draft visibility.
The old gate ran firefox --headless --screenshot, which renders fine even
when the juggler automation layer is missing from the package — so a binary
Playwright can't actually drive (firefox-8) passed and shipped broken.
Replace it with a real drive gate: a 5-leg matrix that launches each binary
over the juggler pipe on its native runner, loads a page, and round-trips JS
(also asserts navigator.webdriver stays hidden). Headless and no screenshot,
so it stays GPU-free on the hosted runners and needs no proxy or secrets.
Same logic is reusable standalone via verify-assets.yml to drive-test an
existing release's assets without a rebuild.
release.yml builds linux-x64/arm64 + win-x64 (cross) on free Linux runners and
macos-arm64/x64 on native Mac runners; packages per the wrapper contract
(juggler-gated so binaries are Playwright-drivable, issue-#14 symlink-safe via
cp -aL), validate_release.py gate, ad-hoc macOS codesign, DRAFT publish.
constants.py: arm64 + darwin ARCHIVE_NAME + BINARY_ENTRY_REL (Firefox.app).
download.py: macOS post-extract xattr quarantine strip.
BINARY_VERSION unchanged (firefox-8); the juggler-fixed firefox-9 is a separate
release cut + pin bump.
firefox-8 carries the WebRTC fixes: behind a proxy, ICE now completes with an
mDNS .local host and a server-reflexive candidate on the proxy IP (genuine
nICEr priority/foundation) instead of coming up blocked, and IPv6 host
candidates are suppressed. Binary published on the releases page; validated on
both Windows and Linux via scripts/validate_release.py.
Unit sentinels (run in CI) assert the rules a real WebRTC profile must meet:
host candidate is mDNS .local (never a raw LAN IP), the synthetic srflx carries
the egress IP with a genuine nICEr priority (rejecting the old local_pref
0xFFFF) and a foundation distinct from the host one, and CreepJS's resolver
returns the egress (and reads a host-only SDP as blocked).
e2e tests launch the binary and check the live gather. "Behind a proxy" is
reproduced without any external proxy: an in-process SOCKS5 server relays TCP
CONNECT but refuses UDP ASSOCIATE (a TCP-only residential proxy), and the
egress IP is injected as an RFC 5737 TEST-NET address.
test_not_blocked_behind_tcp_only_socks guards the gather-fails-behind-proxy bug.
webrtc-e2e.yml runs the e2e on demand (needs a binary that carries the fixes).
Refine timezone="auto" so it ALWAYS resolves (drop the "host" sentinel):
- ""/"auto" resolve from the proxy egress when a proxy is set, else from the
host own public IP (direct lookup); an explicit zone is the only opt-out.
- on failure: with a proxy raise; without a proxy fall back to the host TZ.
GeoIP DB now auto-updates against daijro/geoip-all-in-one weekly rebuild:
cache the latest, re-check after GEOIP_REFRESH_DAYS (7), prune old tags,
reuse a stale cache offline; GEOIP_MMDB_VERSION is only the cold fallback.
tests: test_geo.py (37) + test_geoip_update.py; full unit suite 429 green
plus 8 live combinations (proxy / no-proxy / explicit / failing / freshness).
Follow-up to the timezone="auto" feature. The launcher/config timezone
docstrings still said "empty means host TZ", now incomplete since
""+proxy defaults to auto.
- launcher timezone arg: full precedence (auto default, host escape
hatch, fail-early on unresolvable zone behind a proxy)
- config.get_default_stealth_prefs: note it does NOT resolve "auto"
(pure pref builder, no proxy/network context)
- CHANGELOG: Unreleased entry for timezone="auto" + new deps
A proxy in a different country paired with the host timezone is the
classic timezone_mismatch signal, so a session with a proxy and no
explicit timezone now resolves the zone automatically.
- discover the egress IP through the proxy (SOCKS via requests[socks]),
map it to an IANA zone with an offline mmdb (daijro/geoip-all-in-one,
downloaded + cached like the Firefox binary; GPL so not vendored)
- precedence: explicit zone wins; ""+proxy and "auto"+proxy resolve;
""/"auto" without a proxy stay host; "host"/"local" force host TZ
- fail-early when a proxy is set but the zone cannot be resolved, never
a silent host-TZ fallback
- deps: requests[socks], maxminddb, tzdata (zoneinfo ships no DB on Windows)
- resolve_session_timezone / ensure_geoip_mmdb exported for integrations
Live smoke test caught a footgun: passing headless=True directly to
playwright.firefox.launch() with our prefs puts Firefox in true
headless mode (no rendering pipeline) which breaks canvas/audio/WebGL
fingerprint coherence. InvisiblePlaywright translates user-facing
headless=True to Playwright headless=False + virtual display
automatically; the new public helpers do not, so the docstring +
README now flag this explicitly.
Verified: same prefs + headless=False via firefox.launch() reaches
bot.sannysoft.com with 23 passed / 0 failed, matching what
InvisiblePlaywright produces.
Adds invisible_playwright.config module with:
- get_default_stealth_prefs(seed, *, pin, locale, timezone,
extra_prefs, humanize, virtual_display) -> dict
- get_default_args() -> list
Both also re-exported at the package root alongside the existing
InvisiblePlaywright. ensure_binary is also re-exported there for
parity with the cloakbrowser.download.ensure_binary integration
pattern that downstream projects (Skyvern PR #5340, crawlee-python
PR #1794, agno PR #8129) already expect.
These helpers let third-party fetchers (changedetection.io plugins,
Crawlee BrowserPool subclasses, agno toolkits) drive
playwright.firefox.launch(executable_path=..., firefox_user_prefs=...)
themselves without depending on the InvisiblePlaywright context
manager owning the lifecycle. Same seed semantics, same humanize
toggle, same extra_prefs overlay as the existing wrapper.
Tests: tests/unit/test_config_public.py adds 14 unit tests covering
deterministic seed, locale/timezone/pin/extra_prefs/humanize
variations, and round-trip via the public namespace. Full unit suite
(392 tests) stays green.
Backwards compatible: InvisiblePlaywright surface is unchanged.
BINARY_VERSION stays at firefox-7. Python-only release.
Carries the #24 fix (importlib.metadata-driven __version__ + top-level
--version flag) so users see a real version bump after `pip install --upgrade`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs reported in #24:
1. `python -m invisible_playwright version` printed the literal "0.1.0"
regardless of the installed version. Root cause: __version__ in
__init__.py was hardcoded and never bumped when the package version
moved past 0.1.0. Fix: read from importlib.metadata so __version__
stays in lockstep with pyproject.toml's `version` field by construction.
2. `python -m invisible_playwright --version` errored with "the
following arguments are required: cmd". Root cause: the parser had
`required=True` on its subparsers and no top-level --version flag.
Fix: add a top-level `--version`/`-V` flag using argparse's standard
version action, drop `required=True`, and reroute the "no subcommand"
case through parser.error() so the existing test_no_subcommand_errors
contract is preserved.
7 new unit tests pin both behaviours so they can't regress silently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaced em-dashes (—) with commas, colons, or periods depending on
context. Kept all emoji (✅/❌/⚠️) in the comparison table since
those are scannable scoring cues, not stylistic.
Net cleanup: 6 em-dashes removed from tagline, hero alt-text,
"Why it's powerful" paragraph, and the comparison intro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous build still had ~25px of dark padding above the screenshot
plus a thin shadow border around it. Both gone now. Screenshot is
flush against the top edge of the frame and runs full-width; the only
remaining chrome is the 90px caption bar at the bottom with the green
checkmark and detector verdict.
Net effect: roughly 25-30% more usable screenshot area per frame, no
visible "bar" above. Size 478 KB (was 381 KB, larger because the
embedded pixels are now bigger).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "invisible_playwright" / github URL strip at the top of the
hero was redundant - the README headline right above the embed
already says both. Removing it frees vertical pixels for the actual
screenshot, which is what the visitor is here to see.
Layout now: screenshot fills everything above the 90px caption bar
at the bottom. Each screenshot renders ~10% larger.
Size 381 KB (was 356 KB - slightly larger because the screenshots
themselves are bigger after the scale change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two trailing paragraphs (testing disclaimer and the
"if you need Firefox + active maintenance" wrap-up) restated what
the table already showed. The table is the comparison; let it stand
on its own and jump straight to Install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous wording lumped CloakBrowser in with Camoufox as
"open-source peers." That was wrong: CloakBrowser publishes a Chromium
binary plus a wrapper, but the C++ source patches that produce the
binary are not in the repo. From a user's standpoint that's the same
trust profile as a closed-source commercial fork.
Comparison table updated to reflect this: Open source column now
distinguishes Camoufox (MPL, full source) and ours (MIT, full source
in invisible_firefox) from CloakBrowser (binary only) and the four
SaaS competitors (closed).
Intro paragraph rewritten to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero GIF already shows each detector with its verdict in-frame
(reCAPTCHA 0.90, CreepJS 0 lies, FingerprintJS Pro not detected,
WebRTC no leak, sannysoft all green). Repeating the same five points
as a bullet list immediately below the visual was duplication that
pushed the comparison table and install snippet further down.
Above the fold is now: tagline, hero GIF, then straight into Why
it's powerful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hero GIF above the fold already lists each detector with its
verdict in-frame. The Results section was repeating the same five
items in prose right after the visual, which is the kind of
above-the-fold padding that pushes the install snippet further down
without adding signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hero is a 5-frame slideshow (12.5s loop, 356KB, 1200x675) cycling
through the five detection-test screenshots with a green-check
caption per frame. Branded with the project name top-left and a
github URL top-right so it works when embedded in tweets or blogs
without context.
Sits immediately after the tagline so the first thing a visitor sees
above the fold is moving visual proof, before any text. Industry
research on top-performing OSS READMEs in 2026 consistently puts
GIF/video first as the single biggest conversion lever for star intent.
Also collapsed the five expanded Results subsections down to a single
section: the GIF already shows each tester live, so the prose now
gives one short bullet per detector plus links to the original
full-resolution screenshots for anyone who wants to inspect them.
Net effect: roughly 200 lines of vertical scroll removed above the
fold, hero visual added, deep-link to per-tester screenshot
preserved.
No new test surface. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes aimed at making the project's value legible in 5 seconds
for someone landing from a Trending list or HN thread:
- New tagline + quantified bullet hook at top. Concrete numbers
(0.90 reCAPTCHA, 0 CreepJS lies, 5/5 detection suites passed) up
front instead of generic "passes the hardest detectors" wording.
- Comparison table rewritten. The commercial-stack columns (Multilogin,
GoLogin, AdsPower, Dolphin Anty, Kameleo) were noise for the OSS
audience we want to reach. Replaced with the two relevant
source-level peers: Camoufox (Firefox, currently in a long
maintenance gap) and CloakBrowser (Chromium, fresh, but capped at
the Chromium reCAPTCHA ceiling).
- "Why it's powerful" opens with explicit positioning vs the two
peers, plus the reCAPTCHA v3 score that's the most defensible
numeric claim.
No code change. Repo homepage URL set separately via API to deep-link
the "Why it's powerful" section so visitors from external links land
on the value pitch rather than the badges row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
firefox.exe --version on Windows prints the version string but may
return non-zero exit code (sub-process fork quirk). The previous check
treated that as a launch failure, producing a false-positive failure
across the whole matrix while the binary actually launched cleanly.
Switch to matching the printed output instead, so we only fail when the
binary really can't start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triggered by issue #22 (firefox-7 SxS mismatch reported on Win11 26200
by jannusdorfer-create). Verifies the shipped binary launches cleanly
on every Windows runner GitHub offers: windows-2022, windows-2025,
windows-latest, across Python 3.11 / 3.12 / 3.13.
Each cell does the reporter's exact flow: fresh checkout, pip install
from source, python -m invisible_playwright fetch, then runs the
InvisiblePlaywright(seed=9128) snippet.
If all cells pass the bug is environment-specific to the reporter
(corporate edition, EDR, GPO). If any cell fails the same way we ship
a sidecar mozglue.manifest in the next release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds opt-in helper that auto-injects coherent cookie history into every
BrowserContext created via new_context(). Content is fully deterministic
from the persona seed so a given seed always presents the same cookies
across sessions.
Composition (per persona, all derived from seed):
- 5 cookies on .google.com (NID, CONSENT, SOCS, _GRECAPTCHA, ENID).
Excludes 1P_JAR which was deprecated by Google in 2022. CONSENT
`lang+region` token derived from the persona's IANA timezone
(Europe/Rome -> it+IT, America/* -> en+FX, etc.). NID prefix
broadened to 100-540 to cover historical versions.
- Per-site cookies on 13-25 "visited" everyday domains, sampled from a
Bayesian network conditioned on gpu_class - workstation/high_end
personas trend toward dev/tech sites, low_end/integrated_old trend
toward shop/news/reference. Each site contributes 1-7 cookies based
on a `cookie_profile` tag. Cookie pool includes _ga, _gid, _clck,
_clsk, __cf_bm, OneTrust/CookieYes consent, _fbp (Facebook Pixel),
_dc_gtm_<id> (Tag Manager helper), __hssrc (HubSpot helper).
API:
Stealthfox(seed=42, prep_recaptcha=True)
No per-call configuration: visited-sites + cookie composition all derived
from the persona seed via the Bayesian sampler.
Gated server-side: forced False if profile_dir is set (persistent profile
owns its own state). All expiries capped to 395 days per Chrome/Firefox
400-day RFC 6265bis-15 limit.
Bayesian integration:
- New `derive_browsing_history(gpu_class, rng)` in _fpforge/_sampler.py
(parallel to `derive_font_prefs`).
- New data files: browsing_pool.json (50 site entries) and
cpt_browsing_given_class.json (per-class probabilities).
- Profile dataclass exposes `browsing_history` field.
- _recaptcha_seed.py consumes Profile.browsing_history; receives
timezone separately to derive CONSENT lang+region.
Also drops a dead Chromium-only e2e test that always skipped on our
Firefox-only wrapper.
Test coverage: 29 unit tests covering composition, profile recipes
(minimal/ga_only/ga_cf/ga_consent/ga_consent_clarity), determinism,
Chrome 400-day cap, Playwright field requirements, CONSENT lang
mapping (IT/DE/US/default), helper-cookie probability distributions,
end-to-end with real fpforge Profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The per-asset shields.io endpoint rendered the asset filename next to
the count ('1 [launch.txt]'). Switching to the per-tag total endpoint
renders as just the integer.
Pulls the github download_count of the companion repo's usage-counter
asset. Click-through lands on the invisible_firefox release where the
full disclosure of what the counter measures lives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The companion Firefox source-fork repo was renamed today from
feder-cr/invisible-firefox to feder-cr/invisible_firefox so the two
canonical project repos share the same underscore naming
(invisible_playwright + invisible_firefox).
GitHub redirects clones of the old URL transparently, so anyone with
an existing clone keeps working without changes. New clones go
through the underscore URL directly.
This commit updates all in-repo references (README, CHANGELOG,
CONTRIBUTING, SECURITY, ISSUE_TEMPLATE/config.yml) to the new name.
No code, no version bump, no behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-part cleanup:
- CHANGELOG.md #18 entry: rewrite the symptom description without
naming the specific third-party site that originally reported it.
The technical root cause and fix are unchanged.
- README.md: remove the entire "Known issues" section. Its only entry
was the headless=True alt-desktop crash from #18, which was fully
fixed in 0.1.7 / firefox-7. Leaving the workaround instructions in
the README would have misled users into adopting them
unnecessarily. New issues can be added back as they're found; the
default state is "no known issues".
Pre-push: 402 unit + integration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin fission.webContentIsolationStrategy=0 (IsolateNothing) in baseline
prefs. FF150 ships strategy=1 (IsolateEverything) by default, which
site-isolates every cross-origin iframe into a separate webIsolated
content process even when fission.autostart=False - different from
FF146 Playwright behavior. The parent's Juggler FrameTree then sees
the iframe placeholder with no docShell, no URL and a stale execution
context, so content_frame() returns None, frame.evaluate() throws
cross-origin permission errors, and frame_locator(...).click() times
out. Reproduced with a local cross-origin HTTP harness (two 127.0.0.1
ports = two SOP origins).
The fix is a single pref because that's the level the original Juggler
code paths assume - vanilla Playwright Firefox 146 ran with the looser
default. Disabling site-isolation costs nothing for a single-user
puppet browser; process-per-browser/profile isolation is unaffected.
Caller can A/B per session via
extra_prefs={"fission.webContentIsolationStrategy": 1}.
Tests: tests/test_cross_origin_iframe.py adds 4 unit sentinels (pref
in baseline, survives translate_profile_to_prefs, extra_prefs override
works) and 5 e2e sentinels that spin up two local HTTP servers on
random free ports and verify the four protocol operations that
regressed (page.frames URL tracking, content_frame(), frame.evaluate(),
frame_locator(...).locator(...)) plus dispatch_event('click')
end-to-end, for plain, sandboxed and titled iframes. A future FF
upgrade or A/B flipping the pref will fail the suite before shipping.
Verified clean: pytest -m unit (342 passed), fppro_full.py (ALL
CRITICAL FLAGS CLEAN), fppro_consistency.py (visitor_id stable).
BINARY_VERSION stays firefox-7 - Python-only release.
Issue: https://github.com/feder-cr/invisible_playwright/issues/20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single bug_report.yml with 3 templates that each require
only the fields relevant to their bug category. The old form asked every
reporter for URL / selector / runnable repro snippet, which made no sense
for launch failures (no browser to navigate, no element to select) and
overshot for detection reports (proxy region matters more than selector).
01-launch-failure.yml browser or wrapper never reaches new_page
02-site-or-action-bug.yml browser starts, an op (click / navigate / eval) fails
03-stealth-detection.yml a detector flags the browser (FpJS, CreepJS, BotD, ...)
Each template asks for the env basics plus the fields specific to its
category. The site/action one keeps the strict requirements introduced in
the previous template revision (headless, proxy, profile_dir, URL, selector,
runnable repro). The detection one drops selector/profile and asks for
detector name + verdict paste + screenshot. The launch one drops URL /
selector / proxy and asks for install command + full traceback.
Also fixes the contact_links: the old "Bug in the patched Firefox itself"
link pointed to feder-cr/firefox-stealth, a repo that was deleted on
2026-05-19 when source moved to feder-cr/invisible-firefox. Updated to
the new URL and clarified the boundary with the new stealth detection
template (detection results stay in this repo, source-level C++/IDL
patches go in invisible-firefox).
Re-shapes the bug report form so reporters cannot submit underspecified
tickets like #18 / #19 / #20 which all required multiple round-trips
asking for basic environment context. Every field is now required unless
explicitly marked optional.
New required fields:
- headless=True/False (or N/A) — many bugs only repro on Windows
headless=True where we use a hidden alt-desktop
- Proxy region (no proxy / UK / US / other / datacenter) — site
behavior is often geo-dependent (e.g. GDPR consent on UK only)
- Profile directory (transient / persistent) — affects cookie state
- Exact URL string (no more "the homepage")
- Exact selector / locator (for click / locator bugs)
- Runnable reproduction Python snippet (pre-filled with scaffold so
reporters cannot submit "just open the link")
Existing fields preserved: version, OS, Python, expected, actual,
confirmations.
New optional fields: screenshot upload area, browser/Playwright DEBUG
logs textarea, free-form notes.
Bumps BINARY_VERSION to firefox-7 which ships the real fix for issue #18
(id.sky.com tab crash on Windows headless=True).
firefox-7 contents:
- Canvas2D getImageData stealth spoof moved from read-only mapped surface
to JS Uint8ClampedArray writable buffer (commit 2e17b4871f93 in
invisible-firefox). Fixes the segfault on GPU-backed canvases that hit
during page unload.
Paired with the wrapper-side security.sandbox.content.level=4 workaround
shipped in 2e0adbd (this repo), the combination resolves both halves of
issue #18: cross-process navigation crash in loop AND segfault at close.
End-to-end tested with InvisiblePlaywright headless=True + UK proxy on
id.sky.com: page survives 30s+, no crash in loop, no crash at close.
firefox-6 was rolled back when its partial-fix hypothesis turned out to
be the wrong root cause. Skipping straight to firefox-7.
- pyproject.toml: norecursedirs for tests/playwright-upstream/, a vendored
Microsoft Playwright test suite with its own pixelmatch API version
mismatch. We run it explicitly when doing compat audits, not on every
push. Default collection now ignores it so the pre-push hook (which
runs the full default pytest collection) doesn't error out.
- tests/test_service_worker.py: replace em-dash with hyphen inside a
bytes literal at line 91. Python rejects non-ASCII bytes literals
with SyntaxError at collection time. Now collects cleanly.
Both were blocking unrelated pushes (e.g. the issue #18 fix in the
previous commit). Splitting them out so the issue #18 commit stays
focused.
Two-part fix for the wrapper-repo issue #18 (tab crash on id.sky.com
and similar sites with cross-process navigation):
Part 1 (this commit, wrapper-side):
- _WIN_VIRT_DESKTOP_WORKAROUNDS adds security.sandbox.content.level=4.
When the Chromium sandbox runs at content level >4 (default 6) it sets
STARTUPINFO.lpDesktop=kAlternateWinstation for the content process,
putting it on a different desktop than the browser process. Combined
with our hidden alt-desktop (CreateDesktop) for headless windows
hiding, that means cross-process navigations (Adobe AppMeasurement
triggers a new origin -> new content process) can't reparent windows
across desktops; the new content process exits cleanly and Playwright
fires page.on('crash'). Lowering content sandbox to 4 keeps content
processes on the parent's desktop. Level 4 still blocks file/registry/
network access; only the alt-winstation isolation is dropped, which
is what the desktop bug requires.
- README.md adds a Known Issues section pointing at issue #18 with
the workaround (headless=False or Linux+Xvfb) for users on wrapper
versions before firefox-7.
Part 2 (separate commit in invisible-firefox@2e17b4871f93):
- CanvasRenderingContext2D::GetImageDataArray moved the stealth pixel
noise from rawData.mData (read-only DataSourceSurface::Map) to the
JS Uint8ClampedArray's backing buffer. The original write to a
read-only mapped surface segfaulted on GPU-backed canvases during
browser.close() teardown.
Verified end-to-end with InvisiblePlaywright headless=True + Evomi UK
proxy on id.sky.com: page survives, no crash in loop, no crash at
teardown.
Reporter: @gamefireat123-eng.