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).
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.
Audit follow-up (2026-06-10), all validated before commit.
#2 WebRTC — the shipped baseline now MATCHES the manually-validated config
(behind a residential proxy: host=<uuid>.local, srflx=proxy egress, No-Leak,
gathering completes, indistinguishable from vanilla Firefox on BrowserLeaks +
CreepJS):
- prefs baseline obfuscate_host_addresses False->True; add
zoom.stealth.webrtc.disable_ipv6=True; drop the dead
media.peerconnection.ice.disableIPv6 (no-op on FF150)
- launcher auto-derives the proxy egress IP via _geo.prepare_session_geo
(one round-trip shared with the timezone resolution) and feeds nICEr via
STEALTHFOX_WEBRTC_PUBLIC_IP + STEALTHFOX_WEBRTC_DISABLE_IPV6 in _build_env
(sync + async); an explicit caller env still wins. The C++ mechanisms were
already in firefox-9 — this activates them, no rebuild.
#1 drop orphan prefs zoom.stealth.timezone + zoom.stealth.seed (read by no C++;
the live ones are juggler.timezone.override + zoom.stealth.fpp.hw_seed).
#3 release title 'rev N' instead of 'rev firefox-N'.
CI guards (unit, leak-safe — no real proxy/creds, the kind that would have
caught this gap at zero cost):
- shipped-baseline guard + no-orphan-prefs (test_webrtc_realness.py)
- egress auto-derive in _build_env (test_launcher_helpers.py)
- prepare_session_geo returns (tz, egress) (test_geo.py)
CI keeps faking 'behind a proxy' with an in-process TCP-only SOCKS5 + RFC 5737
TEST-NET IPs; real-proxy residential realness stays a LOCAL manual gate.
449 unit pass.
Maximize what's tested on CI: a new e2e job fetches the public firefox-9 binary
and runs all 138 @pytest.mark.e2e via scripts/run_e2e.py under xvfb-run. The
wrapper launches Firefox HEADED on a (virtual) display, which is stable on the
hosted runners — unlike the true-headless drive-gate that flaked. Secret-free
(public binary + local fake-SOCKS for webrtc), so safe in public CI; the proxy
realness gate (fppro) stays local. Trial-running now; will tune if xvfb/timeouts
need it.
B6: pin every third-party action in the build/publish path to an immutable
commit SHA (a retagged actions/checkout or action-gh-release would otherwise
inject code into the binary users download). The other workflows (tests, webrtc,
launch-matrix) handle no secrets, so they're left on tags.
B4: the playwright pin lived in two workflow files with no shared source. Move
it to scripts/playwright_pin.txt that both read, so they can't drift. The drive
gate already ENFORCES playwright<->juggler compatibility (an incompatible pin
fails the launch/drive and nothing publishes); the file is the single bump point
when the juggler is re-synced.
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.
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.
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).
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>
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>
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.