[pull] main from daijro:main - #13
Merged
Merged
Conversation
(cherry picked from commit bd319ec)
maybe_download_addons() treated an addon as already downloaded whenever its directory existed. A download that fails partway leaves an empty directory behind, which is then trusted on every later launch, so confirm_paths() raises InvalidAddonPath: manifest.json is missing and never recovers. Gate the check on manifest.json presence and rmtree the partial directory on failure. Closes #308. (cherry picked from commit 0a82119)
check_asset() already reads the asset's digest from the GitHub API and stores it as installed_sha256, and AvailableVersion carries a sha256 field through to version.json. Nothing compared either against the bytes that were downloaded: every sha256 equality check in the package compares metadata to metadata when selecting an installed version, and hashlib appeared only in utils.py to key a config cache. So the archive that gets extracted over the install directory, and then chmod 755'd and executed, was accepted on transport security alone. The digest needed to catch a substituted or truncated asset was already in hand and unused. Add verify_sha256() and call it between download and extraction on both install paths -- install_versioned() for the CLI and InstallWorker for the GUI. It hashes in 1 MiB blocks so a multi-hundred-megabyte asset does not have to be held in memory, and rewinds the buffer afterwards so unzip() still reads from the start. When no digest is published the install proceeds with a warning rather than failing: some sources publish no digest, and refusing to install from them would be a regression, not a fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 65cda21)
public_ip() called requests.get with verify=False and wrapped it in a context manager that silenced urllib3's InsecureRequestWarning, so the disabled verification produced no output either. These requests are routed through the user's proxy, which is the exact position an attacker occupies. A forged response controls the value public_ip() returns, and that value is used to spoof the WebRTC IP -- so the leak the function exists to prevent becomes attacker-selectable. validate_ip() bounds this to a well-formed address, but the address is still theirs to choose. Set verify=True and drop the warning suppression. requests raises SSLError, a subclass of RequestException, which the existing loop already catches -- a host with a bad certificate is now skipped in favour of the next one in URLS instead of being trusted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 9de9751)
(cherry picked from commit 9782706)
…ster - Add service-tester/run_tests.ps1: PowerShell equivalent of run_tests.sh, mirrors all three binary modes (local | fetched | both), auto-detects Windows build at obj-*-windows-msvc/dist/bin/camoufox.exe, handles Windows venv paths (.venv\Scripts\python.exe), and sets UTF-8 console encoding so Unicode box-drawing characters render correctly - Fix service-tester/_bundle.py: use esbuild.cmd on Windows instead of the Unix shell script esbuild (WinError 193 otherwise) - Fix service-tester/run_tests.py: reconfigure stdout/stderr to UTF-8 on win32 to prevent UnicodeEncodeError from box-drawing characters in console output (CP1252 default does not support U+2500) Tested on Windows 11 (build 26200) — all three -Binary scenarios (local/fetched/both) behave correctly at the script level. Browser launch is currently blocked by a missing mozglue.manifest in the Windows zip package (SxS resolution fails on clean installs); documented in PR. (cherry picked from commit 2dc1abc)
from_preset() set userAgent, platform and oscpu from the captured device
but never appVersion. Firefox reports appVersion as "5.0 (<OS tokens>)",
so leaving it unset let the host's own value through — and a page reading
two properties saw them disagree.
Measured on 152.0.4-beta.29, macOS host, os="linux", fingerprint_preset:
navigator.platform Linux x86_64
navigator.appVersion 5.0 (Macintosh) <- the host
The value is derived from the user agent rather than from the platform,
because 20 of the 65 bundled Linux presets carry a distro token
("X11; Ubuntu") that a platform lookup would flatten to "X11" — a smaller
mismatch than the host leaking, but the same kind. Firefox builds
appVersion from the same OS tokens as the UA, minus the architecture and
the Gecko revision, with Windows collapsed to its family name; checked
against 800 browserforge fingerprints, the derivation is exact on every
one, including Android and the Ubuntu variant.
A preset that ships its own appVersion keeps it, and a user agent the
rule cannot parse leaves the key unset rather than inventing a value.
(cherry picked from commit 759e4ab)
navigator.maxTouchPoints was already a recognized config property (settings/properties.json declares it) but nothing read it, so it always reported 0 on a desktop build regardless of the config. Under a Firefox-for-Android user agent that 0 is a mobile-detection tell: a real phone reports 5, and Firefox's RFP path only ever collapses the value to 0 (MaxTouchPointsCollapse) — there is no path to a phone value without an explicit override. Add the standard MaskConfig early-return at the top of Navigator::MaxTouchPoints, matching how other navigator/screen properties are spoofed. Navigator.cpp already includes MaskConfig.hpp via navigator-spoofing.patch, so no extra include or schema change is needed. Build-verified: applies cleanly against firefox-152.0.4 and compiles for linux/arm64. Tested on the built binary — config value 5 -> 5, 1 -> 1, no config -> 0 (unchanged desktop default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 394b19f)
canPlayType() and isTypeSupported() leak which system codec libraries
(FFmpeg, VideoToolbox, GStreamer) are installed by checking
PDMFactory::Supports(). This patch bypasses those checks in
MP4Decoder::IsSupportedType() and MatroskaDecoder::IsSupportedType()
when the media:spoof_codecs config key is set.
Gated behind MaskConfig::GetBool("media:spoof_codecs").value_or(false)
so default behaviour is unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 9331de3)
The gfx font lookup paths need to consult the spoofed "fonts" list on every family resolution, so it cannot be re-parsed from CAMOU_CONFIG per call. (cherry picked from commit 2a9ee87)
font.system.whitelist makes ApplyWhitelist() delete non-listed families from the process-wide font list, and with the shared font list that pruning happens in the parent process -- the one that paints the browser chrome. On Windows the titlebar buttons lost Segoe Fluent Icons and drew tofu boxes for U+E921/E922/E8BB. Filter on FontVisibilityProvider::IsChrome() instead, in the three content-reachable paths: FindAndAddFamiliesLocked, GlobalFontFallback and gfxUserFontSet's src: local(). Fixes #695 (cherry picked from commit 870a5f5)
Chrome CSS is `font: message-box`, so the browser UI was handed "Segoe UI" on Linux and "-apple-system" on Windows -- families that aren't installed, leaving the toolbar in the default serif font. The GTK and cocoa LookAndFeel hooks are dropped: with the ComputeSystemFont guard they are only reachable when navigator.platform is unspoofed (no-op) or the document is chrome (harmful). Refs #695 (cherry picked from commit 6584932)
…ary row (#751, #752) `sendOne()` converts a relative coordinate to an absolute one as `eventY + boundingBox.top`, so a relative y of 0 dispatches at absolute y == boundingBox.top exactly -- the content area's first row. The chrome above the content is a fractional number of CSS pixels tall, and the widget rounds the coordinate to a whole device row before hit-testing it. Wherever round(top) < top that rounded row still belongs to chrome, so the event fires as an exit event rather than eMouseMove, no juggler-mouse-event-hit-renderer ack is produced, and -- because dispatch is serialized on activateAndRun()'s process-global chain -- that one missing ack wedges every later input event in the process forever. Same deadlock as the far-edge (#225) and no-op-move cases, from the near edge. The chrome height is a deterministic function of the spoofed OS, so this is not flaky, it is per-fingerprint. Measured on v152.0.4-beta.30, headless Linux, `page.mouse.move(31, 0)` from an interior point: os boundingBox.top rounds to result windows 51.4 51 (above) hangs, 5/5 macos 53.1 53 (above) hangs, 5/5 linux 56.5 57 (below) completes, 8/8 Relative x == 0 is unaffected because boundingBox.left is a whole 0. The far edges were fixed by treating them as out-of-viewport; 0 cannot be. It is a legitimate in-viewport coordinate, and the out-of-viewport branch returns silently for mousedown/mouseup -- so widening the bounds check would convert the hang into a click that reports success and fires nothing, which is what #752 describes. Snap the dispatched coordinate to the first whole pixel inside the browser element instead: it stays within content pixel 0 and lands clear of the boundary. humanize is what makes this common in the field rather than the cause of it. Every PageHandler starts at _lastTrackedPos = {x: 0, y: 0}, so a session's first humanized move always departs from the top-left corner and the curve rides the y==0 row. On a stock build a first humanized click hung on 5 of 20 cold pages; all five had dispatched a point at y==0, and the 15 that completed had dispatched none. With the fix, 0 of 20 hung and all 8 y==0 points dispatched across those runs were acked. Add tests/patches/near-edge-mouse-deadlock.py, which covers all three spoofed OSes so it does not depend on which offset a given host's chrome produces, and both the direct and humanized dispatch paths. It fails on stock at the first move and passes with the fix. humanize-edge-deadlock, humanize-mouse-trajectory, noop-mousemove-deadlock and trusted-events all still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 014cc65)
…achable Four deadlocks shipped between 2026-04 and 2026-09 -- exact-edge coordinates (9270618), humanized trajectory points that bypassed the endpoint's guard (541ffca, #225/#677), a zero-displacement move (16e5a13), and the near edge (014cc65, #751/#752). Each was fixed by adding one more coordinate guard at one more call site. That does not converge, for two reasons. The trigger set is not enumerable. Whether relative y == 0 reaches the renderer is decided by Math.round(boundingBox.top) < boundingBox.top -- a rounding accident in the fractional height of browser chrome, which varies with the spoofed OS. No review catches that. And every miss costs the whole process. activateAndRun() serializes input on a chain shared by every tab; EventWatcher.ensureEvent() waited forever. One missing ack wedged every later input event in the process, permanently, at 0% CPU with no diagnostic. #677 shows why review is not the answer: restoring the humanize trajectory meant writing a bounds check, and the one written was a copy of the pre-#225 form, reintroducing a fixed deadlock one day before it was re-fixed. Three changes, in order of leverage. 1. Bound the waits. EventWatcher.ensureEventWithin() gives up instead of waiting forever; MouseDispatch.sendAcked() uses it, drops the event and logs the type, coordinate and browser rect. This alone closes all four historical deadlocks, including on a build with no coordinate fix at all. The 5s deadline is sized from measurement, not intuition. Over 1000+ dispatches: idle content thread p50 0ms / p99 1ms / max 12ms; a thread burning 8ms per event p50 8ms / max 12ms. But the ack is delivered FROM the content main thread, so a page running a 3s synchronous script delayed a legitimate ack by 2849ms. Block length is page-controlled and unbounded, and silently dropping real input on a slow page is the #752 symptom, so the deadline sits above the slowest legitimate ack rather than near the typical one. Bounding each ack is not enough to bound the work: a humanized curve is ~110 points in a single activation-chain slot. sendTrajectoryAcked() abandons the rest of a curve after the first undelivered point -- not a wall-clock budget, which would false-fire on exactly the slow pages the deadline exists to tolerate. activateAndRun() carries a 30s backstop for the other unbounded waits reachable from the same slot (apz-repaints-flushed, TabSwitchDone, the drag path's waits), none of which has failed yet. 2. One chokepoint. additions/juggler/input/MouseDispatch.js owns the relative-to-absolute conversion, the in-viewport predicate and the ack wait. PageHandler's three independent bounds checks and its raw jugglerSendMouseEvent/sendWheelEvent calls are gone; it now passes relative coordinates and never sees a bounding box. Net effect on that vendored file is 92 lines removed against 22 added -- a smaller diff against upstream juggler, since the logic moved into a file we own. Wheel events go through the same conversion, so a wheel at relative y == 0 no longer scrolls the tab strip. 3. Enforcement. scripts/check-input-dispatch.py fails the build if anything outside the chokepoint dispatches synthesized input or does browser-relative coordinate arithmetic. It needs no browser build, so .github/workflows/lint.yml gates every pull request -- nothing was checking PRs before. Two exemptions, both content-process: PageAgent (drag events, already content-relative, no ack) and FrameTree (the ack producer). docs/input-dispatch.md states the invariant. tests/patches/mouse-boundary-sweep.py replaces hand-picked edge targets, which are what let each of the four through: humanize-edge-deadlock.py probes only the far edges, and humanize-mouse-trajectory.py pins os="linux" -- the one fingerprint immune to #751. It sweeps the whole viewport ring across every spoofed OS with humanize on and off, asserting each point is acked AND observed by the page. It depends on change 1 to run at all: without the backstop the first bad coordinate wedges the browser and the sweep dies there. tests/patches/input-ack-backstop.py covers the bounded wait itself, by blocking the content main thread far longer than the deadline -- a legitimate late ack, with no test-only hook in production code. The sweep immediately found a fifth instance, pre-existing and unreported: boundingBox.height is consistently 0.5 CSS px less than the innerHeight the page reports, so the page's last row is half covered. With the box at 1920x977.5 +0+56.5, relative y == 977 -- innerHeight - 1, well inside the viewport as far as the page is concerned -- dispatches at 1033.5, rounds to 1034, and the content ends at 1034. Deterministic, 4/4, and it deadlocks a stock build. Fixed by clamping to the last whole pixel inside the element, symmetric with the near-edge snap; both live in the one conversion now. All seven patch tests pass: mouse-boundary-sweep (150 ring coordinates over 6 scenarios), near-edge-mouse-deadlock, input-ack-backstop, humanize-edge-deadlock, humanize-mouse-trajectory, noop-mousemove-deadlock, trusted-events. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 827b98d)
PR #562's `media-codec-spoofing.patch` was written when MP4Decoder.cpp included H264.h and VPXDecoder.h adjacently, with AOMDecoder.h behind `#ifdef MOZ_AV1`. FF152 sorts that block alphabetically and hoists AOMDecoder.h out of the ifdef, so the `#include "MaskConfig.hpp"` hunk found no context and the patch failed to apply -- taking every patch after it in the sequence down with it. Only the include hunk was stale; the IsSupportedType() body hunk applied cleanly. Retargeted onto the PlatformDecoderModule.h line, matching where the same patch already places the include in MatroskaDecoder.cpp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 5debb84)
PR #562 added a `media:spoof_codecs` read on the C++ side -- MaskConfig::GetBool("media:spoof_codecs") in MP4Decoder and MatroskaDecoder -- but never declared the key in settings/. Since validate_config() drops any key it does not recognise, the documented usage was inert: AsyncCamoufox(config={"media:spoof_codecs": True}) -> "Skipping unknown patch media:spoof_codecs : True" The key never reached the browser, so the feature could not be turned on through the supported path at all. Declared in both properties.json and camoucfg.jvv (bool, beside mediaDevices:enabled). The new test is the general form rather than a check for this one key: it scans patches/ and additions/ for MaskConfig::Get*/Has*("key") reads and fails when a key is not declared in settings/properties.json. A patch and its schema entry are two halves of one change, and shipping only one half is a mistake this project has now made in both directions -- canvas:seed (#721) and navigator.maxTouchPoints (#696) were declared but unconsumed; this one was consumed but undeclared. Across the tree the scan finds 63 reads against 109 declared keys, and media:spoof_codecs was the only gap. Note the runtime reads properties.json from the *installed browser bundle*, not the repo, so this fix only takes effect for a build packaged after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv (cherry picked from commit 375b0fc)
PR #315 corrects get_screen_cons()'s inverted guard (`headless is False` -> `headless is True`), which is right on its own. But the call site passes `headless or has_display(env)`, folding two separate questions into one boolean, so with the corrected guard a headful run on a real display now reads as headless and the display bound is skipped: headless=False, has_display=True -> arg=True -> None (want Screen) headless=False, has_display=False -> arg=False -> Screen (want None) That drops the monitor bound for every ordinary headful launch, which is the constraint 2266f27 added for #499 -- a 1366x768 laptop goes back to being handed a 2560x1440 fingerprint and a window drawn past the edge of the screen. Pass `headless` alone and gate on has_display() separately. largest_display() already returns None when there is nothing to probe, so the no-display case still yields None. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GQgHHGRXNp29jr4xQjK7iv
navigator.maxTouchPoints could already be spoofed, but nothing moved with it, so a spoofed digitizer contradicted itself in two places a script reads in one line: (any-pointer: coarse) stayed false, and window.TouchEvent and window.Touch were absent entirely. Restore the aID branch in force-default-pointer.patch so the coarse bit joins the *any-pointer* set, and only when maxTouchPoints > 0. The primary pointer stays Fine|Hover: a touchscreen laptop still drives its trackpad, and reporting (pointer: coarse) would claim a phone while the accompanying desktop UA said otherwise. The host LookAndFeel value is still not consulted -- the capability set must not vary with the machine the browser runs on. Expose the touch interfaces by moving TouchEvent::PrefEnabled only, never LegacyAPIEnabled. dom.w3c_touch_events.legacy_apis.enabled is false everywhere but Android, so a real Windows touchscreen laptop exposes TouchEvent and Touch while 'ontouchstart' in window is false. Matching that shape matters more than exposing the whole touch API: a build that switches touch on wholesale is more detectable than one that does nothing. Rename mobile-fingerprint-spoofing.patch to touchscreen-fingerprint-spoofing .patch, since the rationale is the ordinary Windows touchscreen laptop rather than a phone, and carry the new TouchEvent.cpp hunk there beside the existing Navigator.cpp one. The rename moves it after navigator-spoofing.patch in basename order, so its Navigator.cpp hunk now lands with an offset; verified to still apply cleanly with no rejects. Warn at launch whenever navigator.maxTouchPoints is set, separately from the blanket navigator warning, because the knock-on effects reach past navigator into the CSS pointer media queries and the TouchEvent interfaces. tests/patches/touchscreen-digitizer.py checks all 16 signals and asserts that maxTouchPoints=0 still looks like a machine with no digitizer. It fails on a binary built without this change (13/16) and passes on one built with it. The reference values it carries are RECONSTRUCTED, not captured: the recording from the Dell XPS 15 9510 was not reachable from the build host, so eight values come from the specification and eight from Gecko's own gating logic. Each is marked in the table. Check them against the real capture when the reference machine is available; the capture wins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
…shape SearchService.#init() threw on every launch, on a clean profile, in beta.28 through beta.30: "missing field `recordType` at line 1 column 114". The browser ended up with no search service at all, which also takes out the urlbar's heuristic result. Reported as #737. The issue blames a stale bundled search-config-overrides-v2 dump. It is not that. The reporter retracted that diagnosis after measuring, and both bundled dumps are fine as shipped -- the overrides dump is never even parsed, because setSearchConfig throws on the line before setConfigOverrides is reached. The failing document is this patch's own stub. It returned a search-config *v1* record (appliesTo/webExtension), but the selector is the Rust SearchEngineSelector, which deserializes v2: #[serde(tag = "recordType", rename_all = "camelCase")] enum JSONSearchConfigurationRecords { ... } recordType is that enum's tag, so a record lacking it aborts the whole document. Note the #[serde(other)] Unknown variant: an unrecognised recordType is tolerated, a missing one is fatal. Serialising the old stub gives exactly 116 characters with '}' at column 114 -- precisely where serde reports the missing field, confirming the stub is the document that fails. Return v2 records instead: one engine, plus defaultEngines and engineOrders. An empty configuration is not an option, since getEngineConfiguration() rejects [] with "Failed to get engine data from Remote Settings" and SearchSettings refuses to write without an engine. So the "no search engines" stance is kept by making the single engine inert, with a loopback search URL that cannot leave the machine even if something does submit a search. Required fields were checked against configuration_types.rs in this tree rather than assumed. Verified on linux x86_64: 0 recordType errors, "Completed #init", search.json.mozlz4 written, and the only engine present is the inert "None" that settings/distribution/policies.json already defines as the default. tests/patches/search-service-init.py guards it. This shipped broken through three releases because the failure is a console error on a browser that otherwise starts fine, so the test asserts the observable consequences -- settings written to disk, and the engine list -- rather than only the absence of the error string. It fails on stock beta.30 and passes here. The issue's further claim that this also empties the urlbar drop-down (history, autofill) is untested here; the reporter flagged it as an inference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
None is referenced by anything that builds or tests the tree:
* additions/juggler/TargetRegistry.js.bak -- copy-additions.sh does
`cp -r ../additions/* .`, so this 48K copy was landing in every source tree.
It does not reach omni.ja, so it never shipped to users, but it has no reason
to be in the build either.
* patches/ghostery/Disable-Onboarding-Messages.patch.bak
* patches/librewolf/urlbarprovider-interventions.patch.bak -- neither is picked
up by list_patches(), which globs "*.patch", so both have been inert since
they landed.
scripts/check-input-dispatch.py skips `path.name.endswith(".bak")`, but that
condition is already unreachable: it filters the output of rglob("*.js"), which
cannot yield a name ending in .bak. Left in place rather than widen this commit
into someone else's guard. Verified: the script still reports the same 16 files
scanned, and patch discovery still finds 56 patches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
…capture
The reference values were reconstructed, because the recording sat on the
Windows reference box and was not reachable when the guard was written. It is
now read from that machine, so the table is the measured article.
The reconstruction had the right values but the wrong *set*. Of the sixteen
static signals it listed, fourteen matched the capture exactly. The other two
were invented -- `'ontouchstart' in document` and `... in documentElement`,
both false, both harmless -- and they stood in for two real signals that were
missed entirely:
document.createTouch false. Gated by TouchEvent::LegacyAPIEnabled, the same
gate as ontouchstart, so a real touchscreen laptop exposes TouchEvent while
createTouch stays absent.
window.PointerEvent true, and not gated on a digitizer at all. Pinned so it
cannot start varying.
Both were verified against this build before being written down: it reports
false and true respectively, so the fingerprint matches the device on all
sixteen signals, not merely on the fourteen that were guessed correctly. The
two invented signals are kept as informational output, since nothing measured
backs them.
Captured with tests/assets/touch-reference.html, added here so the recording
can be reproduced on another device: it renders the sixteen static signals plus
an input-event log as JSON. The source device was a Dell XPS 15 9510 on
Firefox 152.0 (Windows NT 10.0, Win64, Win32, 1382x864 @ dpr 2.5), whose
capture also carried 284 input events that this guard does not assert on.
Still fails on a binary without the fix -- now 12/16 rather than 13/16, since
the recorded set no longer includes the two invented always-false signals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W2RfR387Mh1JhZ9LZvkptP
Integration branch: eleven open PRs built and verified together on FF152
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )